From 2fb63fed1f7bd769bb5693a0fda221d4e5f283b2 Mon Sep 17 00:00:00 2001 From: Renn F Date: Mon, 22 Jun 2026 15:53:00 +0200 Subject: [PATCH] docs: add the user-facing MkDocs documentation site Build a complete user-facing documentation site (MkDocs Material) under docs/, served at roboco.dev/docs via a new gh-pages deploy workflow. - Sections: Get Started, The Company, the Tour, Operating the Panel, Choosing & Running Models, Cost & Observability, Optional Subsystems, Configure & Deploy, API Reference, Troubleshooting & Security (55 pages). - mkdocs.yml (Material theme; excludes the agent-facing rag/ corpus, internal scratch, and orphaned stub trees) and .github/workflows/docs.yml (mkdocs gh-deploy to gh-pages). - Retire the stale root usage.md and deployment.md to redirect stubs into the site. - Fix the docs tooling: add the pymarkdownlnt dependency + .pymarkdown.json, run serve-docs/lint-docs/fix-docs under the docs extra, add a build-docs strict gate. - Fix the roboco console-script entry point (cli, not the un-awaited async main). - README: correct the project-structure tree (optimal.py, alembic) and link the docs site. --- .github/workflows/docs.yml | 42 ++++ .gitignore | 4 + .pymarkdown.json | 16 ++ CLAUDE.md | 2 +- Makefile | 14 +- README.md | 7 +- deployment.md | 238 +------------------ docs/api/auth.md | 61 +++++ docs/api/index.md | 25 ++ docs/api/rest-api.md | 119 ++++++++++ docs/api/websockets.md | 65 ++++++ docs/assets/logo.png | Bin 0 -> 219657 bytes docs/company/agent-gateway.md | 49 ++++ docs/company/index.md | 35 +++ docs/company/merge-model.md | 61 +++++ docs/company/org-and-roles.md | 92 ++++++++ docs/company/task-lifecycle.md | 89 ++++++++ docs/deploy/bootstrap-and-seeds.md | 56 +++++ docs/deploy/data-and-migrations.md | 74 ++++++ docs/deploy/deployment.md | 178 +++++++++++++++ docs/deploy/env-reference.md | 248 ++++++++++++++++++++ docs/deploy/index.md | 31 +++ docs/get-started/first-project.md | 57 +++++ docs/get-started/first-task.md | 46 ++++ docs/get-started/index.md | 27 +++ docs/get-started/installation.md | 160 +++++++++++++ docs/how-to/01-the-company.md | 2 +- docs/how-to/05-the-business-workflow.md | 2 +- docs/how-to/README.md | 2 +- docs/index.md | 77 +++++++ docs/models/grok.md | 56 +++++ docs/models/index.md | 25 ++ docs/models/provider-routing.md | 60 +++++ docs/models/resilience.md | 43 ++++ docs/operations/cost-and-usage.md | 97 ++++++++ docs/operations/health-and-metrics.md | 58 +++++ docs/operations/index.md | 19 ++ docs/optional/conventions.md | 125 ++++++++++ docs/optional/index.md | 25 ++ docs/optional/pitch-provisioning.md | 56 +++++ docs/optional/pr-review.md | 53 +++++ docs/optional/self-heal.md | 79 +++++++ docs/optional/strategy-engine.md | 46 ++++ docs/optional/toolchain-matching.md | 72 ++++++ docs/optional/web-research.md | 51 +++++ docs/panel/agents-and-work-sessions.md | 50 ++++ docs/panel/auditor.md | 30 +++ docs/panel/business.md | 45 ++++ docs/panel/command-center.md | 63 +++++ docs/panel/communications-and-journals.md | 39 ++++ docs/panel/git.md | 48 ++++ docs/panel/index.md | 22 ++ docs/panel/knowledge-base.md | 43 ++++ docs/panel/metrics.md | 37 +++ docs/panel/projects-and-products.md | 45 ++++ docs/panel/settings.md | 47 ++++ docs/panel/tasks-and-kanban.md | 59 +++++ docs/troubleshooting/common-issues.md | 78 +++++++ docs/troubleshooting/index.md | 19 ++ docs/troubleshooting/security.md | 54 +++++ mkdocs.yml | 182 +++++++++++++++ pyproject.toml | 4 +- usage.md | 267 +--------------------- uv.lock | 177 ++++++++++++++ 64 files changed, 3553 insertions(+), 500 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 .pymarkdown.json create mode 100644 docs/api/auth.md create mode 100644 docs/api/index.md create mode 100644 docs/api/rest-api.md create mode 100644 docs/api/websockets.md create mode 100644 docs/assets/logo.png create mode 100644 docs/company/agent-gateway.md create mode 100644 docs/company/index.md create mode 100644 docs/company/merge-model.md create mode 100644 docs/company/org-and-roles.md create mode 100644 docs/company/task-lifecycle.md create mode 100644 docs/deploy/bootstrap-and-seeds.md create mode 100644 docs/deploy/data-and-migrations.md create mode 100644 docs/deploy/deployment.md create mode 100644 docs/deploy/env-reference.md create mode 100644 docs/deploy/index.md create mode 100644 docs/get-started/first-project.md create mode 100644 docs/get-started/first-task.md create mode 100644 docs/get-started/index.md create mode 100644 docs/get-started/installation.md create mode 100644 docs/index.md create mode 100644 docs/models/grok.md create mode 100644 docs/models/index.md create mode 100644 docs/models/provider-routing.md create mode 100644 docs/models/resilience.md create mode 100644 docs/operations/cost-and-usage.md create mode 100644 docs/operations/health-and-metrics.md create mode 100644 docs/operations/index.md create mode 100644 docs/optional/conventions.md create mode 100644 docs/optional/index.md create mode 100644 docs/optional/pitch-provisioning.md create mode 100644 docs/optional/pr-review.md create mode 100644 docs/optional/self-heal.md create mode 100644 docs/optional/strategy-engine.md create mode 100644 docs/optional/toolchain-matching.md create mode 100644 docs/optional/web-research.md create mode 100644 docs/panel/agents-and-work-sessions.md create mode 100644 docs/panel/auditor.md create mode 100644 docs/panel/business.md create mode 100644 docs/panel/command-center.md create mode 100644 docs/panel/communications-and-journals.md create mode 100644 docs/panel/git.md create mode 100644 docs/panel/index.md create mode 100644 docs/panel/knowledge-base.md create mode 100644 docs/panel/metrics.md create mode 100644 docs/panel/projects-and-products.md create mode 100644 docs/panel/settings.md create mode 100644 docs/panel/tasks-and-kanban.md create mode 100644 docs/troubleshooting/common-issues.md create mode 100644 docs/troubleshooting/index.md create mode 100644 docs/troubleshooting/security.md create mode 100644 mkdocs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..753bc9bf --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,42 @@ +# Builds the MkDocs site and publishes it to the `gh-pages` orphan branch. +# GitHub Pages then serves from that branch (set Settings → Pages → Source: +# "Deploy from a branch" → gh-pages, and configure the custom domain there). +name: docs + +on: + push: + branches: [master] + paths: + - "docs/**" + - "mkdocs.yml" + - ".github/workflows/docs.yml" + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: docs-deploy + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Sync docs toolchain + run: uv sync --extra docs + + - name: Configure git identity for gh-pages + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Build and deploy to gh-pages + run: uv run mkdocs gh-deploy --force --strict diff --git a/.gitignore b/.gitignore index 0bcd4c32..6c83e1e8 100644 --- a/.gitignore +++ b/.gitignore @@ -103,3 +103,7 @@ panel/.env.*.local # Internal-only: strategy/scratch/reference dumps — never publish docs/internal/ .pnpm-store/ + +# MkDocs build output (published to gh-pages by CI; never committed to master) +/site/ +.playwright-mcp/ diff --git a/.pymarkdown.json b/.pymarkdown.json new file mode 100644 index 00000000..0de756d7 --- /dev/null +++ b/.pymarkdown.json @@ -0,0 +1,16 @@ +{ + "extensions": { + "front-matter": { "enabled": true } + }, + "plugins": { + "md013": { "enabled": false }, + "md033": { "enabled": false }, + "md007": { "enabled": false }, + "md030": { "enabled": false }, + "md032": { "enabled": false }, + "md041": { "enabled": false }, + "md046": { "enabled": false }, + "md025": { "enabled": false }, + "md036": { "enabled": false } + } +} diff --git a/CLAUDE.md b/CLAUDE.md index 8aa528d7..c6dbc6b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -536,4 +536,4 @@ ROBOCO_OLLAMA_BASE_URL=http://roboco-ollama:11434 # Native API ## Blueprint Reference -The organizational structure, communication matrix, role descriptions, and access-control model are documented inline above and in the published `docs/` tree (per-area `README.md` files, `usage.md`, `deployment.md`). +The organizational structure, communication matrix, role descriptions, and access-control model are documented inline above and in the user-facing documentation site (MkDocs Material; source under `docs/`, built by `mkdocs.yml`, published to `gh-pages` via `.github/workflows/docs.yml` and served at roboco.dev/docs). `docs/rag/` remains the agent-facing RAG corpus (excluded from the published site); the old root `usage.md` / `deployment.md` are now redirect stubs into the site. diff --git a/Makefile b/Makefile index 0a620d66..ad81949c 100644 --- a/Makefile +++ b/Makefile @@ -401,19 +401,25 @@ high-load-stress-test: # Serve docs .PHONY: serve-docs serve-docs: - @uv run mkdocs serve + @uv run --extra docs mkdocs serve + @find . | grep -E "(__pycache__|\.pyc|\.pyo|\.pytest_cache|\.ruff_cache|\.mypy_cache)" | xargs rm -rf + +# Build the documentation site (strict — fails on broken links / nav) +.PHONY: build-docs +build-docs: + @uv run --extra docs mkdocs build --strict @find . | grep -E "(__pycache__|\.pyc|\.pyo|\.pytest_cache|\.ruff_cache|\.mypy_cache)" | xargs rm -rf # Lint documentation .PHONY: lint-docs lint-docs: - @uv run pymarkdownlnt scan -r -e ./.venv -e ./.git -e ./.github -e ./roboco -e ./tests -e ./.claude -e ./CLAUDE.md -e ./ZZZ . + @uv run --extra docs pymarkdownlnt --config .pymarkdown.json scan -r docs/index.md docs/get-started docs/company docs/how-to docs/panel docs/models docs/operations docs/optional docs/deploy docs/api docs/troubleshooting @find . | grep -E "(__pycache__|\.pyc|\.pyo|\.pytest_cache|\.ruff_cache|\.mypy_cache)" | xargs rm -rf # Fix documentation .PHONY: fix-docs fix-docs: - @uv run pymarkdownlnt fix -r -e ./.venv -e ./.git -e ./.github -e ./roboco -e ./tests -e ./.claude -e ./CLAUDE.md -e ./ZZZ . + @uv run --extra docs pymarkdownlnt --config .pymarkdown.json fix -r docs/index.md docs/get-started docs/company docs/how-to docs/panel docs/models docs/operations docs/optional docs/deploy docs/api docs/troubleshooting @find . | grep -E "(__pycache__|\.pyc|\.pyo|\.pytest_cache|\.ruff_cache|\.mypy_cache)" | xargs rm -rf # Prune @@ -487,7 +493,7 @@ help: @echo " make clean - Clean cache files" @echo " make prune - Prune docker resources" @echo "" - @echo "See docs/deployment.md and docs/usage.md for detailed guides." + @echo "Full docs: https://roboco.dev/docs (preview locally: make serve-docs)" # Python versions list .PHONY: show-python-versions diff --git a/README.md b/README.md index 3b749479..09ca97ec 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ AI Agents Company - A virtual organization of 25 AI agents + 1 human CEO, design > [!WARNING] > **RoboCo is early-stage, work-in-progress software (v0).** It's under active development, runs in a homelab, and *will* have rough edges, breaking changes, and bugs. It is **not production-ready** and the API/database schema are not stable yet. Treat it as a working prototype to explore and build on — please don't expose it to the public internet as-is. Issues and PRs very welcome. +> [!TIP] +> 📚 **Full documentation:** **[roboco.dev/docs](https://roboco.dev/docs)** — install & first run, the company model, a page-by-page panel reference, model providers, the optional subsystems, deployment, and the API. + ## Overview RoboCo implements a structured organizational hierarchy with formal communication protocols, task management, and quality controls. The system enables a single human (CEO) to orchestrate complex multi-project development at scale. @@ -86,9 +89,9 @@ roboco/ │ │ ├── task.py # Task lifecycle management │ │ ├── workspace.py # Multi-agent workspace management │ │ ├── messaging.py # Agent communication -│ │ └── optimal_brain/ # RAG/Knowledge base (in-house pgvector) +│ │ └── optimal.py # RAG/Knowledge base (in-house pgvector) │ ├── models/ # Pydantic domain models -│ ├── db/ # SQLAlchemy ORM & migrations +│ ├── db/ # SQLAlchemy ORM & session │ ├── enforcement/ # Task lifecycle state machine │ ├── runtime/ # Orchestrator for agent spawning │ ├── agents/ # Agent base classes diff --git a/deployment.md b/deployment.md index 5e427e01..c499d2f5 100644 --- a/deployment.md +++ b/deployment.md @@ -1,235 +1,13 @@ # RoboCo Deployment Guide -Deploy RoboCo on your server/NAS. +> **This guide has moved.** Deploying RoboCo is now documented in the full docs site at **[roboco.dev/docs](https://roboco.dev/docs)** (source under [`docs/`](docs/)). -## Prerequisites +Jump straight to: -| Software | Purpose | -|----------|---------| -| Docker | All services (PostgreSQL, Redis, Orchestrator, Agents) | -| Claude Code CLI | Authenticate once on host (creates ~/.claude) | +- **[Install & first run](docs/get-started/installation.md)** — the quickest path: clone, set two secrets, `docker compose up`. +- **[Deployment](docs/deploy/deployment.md)** — the production guide: compose files, the single-origin nginx, host-path mounts, data persistence, secure mode, and startup ordering. +- **[Environment reference](docs/deploy/env-reference.md)** — every `ROBOCO_*` setting with its default and purpose. +- **[Data & migrations](docs/deploy/data-and-migrations.md)** — the schema, pgvector, and how the stack self-migrates. +- **[Troubleshooting](docs/troubleshooting/common-issues.md)** — the common deploy snags and their fixes. -```bash -# Install and authenticate Claude Code (one time) -npm install -g @anthropic-ai/claude-code -claude # Login via browser -``` - -## Quick Start (NAS/Server) - -Everything runs in Docker - no need to install Python/uv on the host. There are two ways to get the stack: **pull the pre-built images** (every release publishes them) or **build from source**. Both start from a clone, which gives you the compose files, the nginx config, and `.env.example`. - -```bash -# 1. Clone the project -git clone https://github.com/rennf93/roboco.git roboco -cd roboco - -# 2. Configure environment -cp .env.example .env - -# 3. Edit .env and set at least: -# ROBOCO_ENCRYPTION_KEY, ROBOCO_AGENT_AUTH_SECRET (secrets; see the file) -# ROBOCO_HOST_PROJECT_DIR=/volume1/roboco (host paths for spawning agents) -# ROBOCO_HOST_CLAUDE_DIR=/root/.claude (or your user's home) -``` - -### Option A — Pull the pre-built images (no build toolchain on the host) - -```bash -docker compose -f docker-compose.registry.yml pull -docker compose -f docker-compose.registry.yml up -d -docker compose -f docker-compose.registry.yml logs -f orchestrator -``` - -Choose the registry and version with `ROBOCO_REGISTRY` (`ghcr.io/rennf93` or `docker.io/renzof93`) and `ROBOCO_VERSION` (`latest` or a pinned release such as `0.8.0`). The orchestrator pulls and spawns the matching pre-built agent images on demand. - -### Option B — Build from source - -```bash -docker compose up -d # builds images on first run, then starts everything -docker compose logs -f orchestrator -``` - -## Architecture - -``` -Your NAS/Server -│ -├── docker compose up -d -│ │ -│ ├── roboco-postgres (pgvector) -│ ├── roboco-redis -│ ├── roboco-ollama (+ ollama-init: pulls the embedding + local LLM models) -│ ├── roboco-orchestrator -│ │ │ -│ │ ├── Runs FastAPI on port 8000 -│ │ ├── Builds (from source) or pulls (registry) the agent images -│ │ └── Spawns agent containers: -│ │ ├── roboco-agent-main-pm -│ │ ├── roboco-agent-be-dev-1 -│ │ ├── roboco-agent-be-qa -│ │ └── ... (each mounts ~/.claude for auth) -│ ├── roboco-panel (Next.js control panel, internal) -│ └── roboco-nginx (single entry point on :3000 → panel + /api,/ws → orchestrator) -│ -├── ~/.claude/ ← Your Claude Code auth (from host) -│ -└── ./data/ ← Persistent data - ├── postgres/ - ├── redis/ - ├── ollama/ ← model cache (multi-GB) - ├── workspaces/ ← per-agent git clones - ├── logs/ - └── mcp-configs/ -``` - -## Configuration - -### Environment Variables (.env) - -```bash -# Host paths - REQUIRED for NAS deployment -# These tell the orchestrator where to find files on the HOST -ROBOCO_HOST_PROJECT_DIR=/volume1/roboco -ROBOCO_HOST_CLAUDE_DIR=/root/.claude -ROBOCO_DATA_DIR=./data - -# Claude Code auth directory (mounted into containers) -CLAUDE_AUTH_DIR=/root/.claude - -# Database (defaults work for docker compose) -ROBOCO_DATABASE_HOST=roboco-postgres -ROBOCO_DATABASE_PORT=5432 -ROBOCO_DATABASE_USER=roboco -ROBOCO_DATABASE_PASSWORD=roboco -ROBOCO_DATABASE_NAME=roboco - -# Redis (defaults work for docker compose) -ROBOCO_REDIS_HOST=roboco-redis -ROBOCO_REDIS_PORT=6379 -``` - -### Customizing Agent Spawn - -By default, the orchestrator spawns `main-pm`, `be-dev-1`, and `be-qa`. To change this: - -```bash -# Option 1: Override in docker-compose.yml -docker compose up -d --scale orchestrator=0 -docker compose run orchestrator --spawn main-pm fe-dev-1 fe-qa - -# Option 2: Edit docker-compose.yml command section -# Uncomment and modify the command line -``` - -## Verification - -```bash -# API health -curl http://localhost:8000/health - -# Orchestrator status (shows running containers) -curl http://localhost:8000/api/orchestrator/status | jq - -# List all RoboCo containers -docker ps --filter "name=roboco" - -# View orchestrator logs -docker compose logs -f orchestrator - -# View agent logs -docker logs -f roboco-agent-main-pm -``` - -## Data Persistence - -All data is persisted to the host: - -| Container Path | Host Path | -|----------------|-----------| -| postgres data | `./data/postgres/` | -| redis data | `./data/redis/` | -| Ollama models | `./data/ollama/` | -| Agent workspaces | `./data/workspaces/` | -| Agent + orchestrator logs | `./data/logs/` | -| MCP configs | `./data/mcp-configs/` | - -For NAS RAID protection, set `ROBOCO_DATA_DIR` to your RAID volume: - -```bash -ROBOCO_DATA_DIR=/volume1/roboco/data -``` - -## Troubleshooting - -### Orchestrator can't spawn agents - -```bash -# Check Docker socket is mounted -docker compose logs orchestrator | grep -i docker - -# Verify host paths are set correctly -docker compose exec orchestrator env | grep ROBOCO_HOST - -# Check if agent image was built -docker images | grep roboco-agent -``` - -### Agent containers exit immediately - -```bash -# Check Claude auth is mounted -docker logs roboco-agent-main-pm - -# Verify ~/.claude exists on host -ls -la ~/.claude/ - -# Re-authenticate if needed -claude -``` - -### Database connection failed - -```bash -docker compose ps postgres -docker compose logs postgres -``` - -### API not responding - -```bash -curl http://localhost:8000/health -docker compose logs orchestrator -``` - -## Stopping - -```bash -# Stop all services -docker compose down - -# Stop all agent containers (if needed) -docker ps --filter "name=roboco-agent" -q | xargs docker stop - -# Full cleanup (removes volumes) -docker compose down -v -``` - -## Development Mode (Host) - -For local development without Docker orchestrator: - -```bash -# Start only infrastructure -docker compose up -d postgres redis - -# Install Python dependencies -curl -LsSf https://astral.sh/uv/install.sh | sh -uv sync - -# Run migrations -uv run alembic upgrade head - -# Start orchestrator directly -uv run python -m roboco.cli --spawn main-pm be-dev-1 be-qa -``` +Preview the site locally with `make serve-docs`. diff --git a/docs/api/auth.md b/docs/api/auth.md new file mode 100644 index 00000000..28f9c873 --- /dev/null +++ b/docs/api/auth.md @@ -0,0 +1,61 @@ +# Authentication + +RoboCo's API identifies a caller by a small set of headers — `X-Agent-ID`, `X-Agent-Role`, and optionally `X-Agent-Team`. How much it *trusts* those headers depends on one flag. Out of the box the API runs in header-trust mode, which is fine on a private LAN and dangerous anywhere else. This page covers both modes and the WebSocket caveat. + +## The identity headers + +Every REST request carries: + +| Header | Required | Meaning | +|--------|----------|---------| +| `X-Agent-ID` | Yes | The agent's UUID or slug (e.g. `be-dev-1`). | +| `X-Agent-Role` | Yes | The role the caller is acting as (e.g. `developer`, `cell_pm`, `ceo`). | +| `X-Agent-Team` | No | The team (`backend`, `frontend`, `uxui`), when relevant. | +| `X-Agent-Token` | Only in secure mode | The HMAC token that proves the headers above weren't forged. | + +These are resolved in `roboco/api/deps.py` (`get_agent_context`), and the role gates the action — so a request claiming `X-Agent-Role: ceo` can do CEO-only things like approving and merging. + +## Header-trust mode (default) + +By default `ROBOCO_AGENT_AUTH_REQUIRED` is unset/false. In this mode the API **accepts the role headers without verifying any token**. There is no proof of identity: whoever sets `X-Agent-Role: ceo` *is* the CEO for that request. + +!!! danger "Anyone who can reach the API can claim any role — including CEO" + In header-trust mode there is no authentication. Any client that can open a connection to the orchestrator port can act as any agent, approve and merge work as the CEO, cancel tasks, or override task state. The app logs a loud startup warning to this effect. This is acceptable **only** on a trusted private network where nothing untrusted can reach the orchestrator — which is the default single-host LAN deployment behind nginx on `localhost:3000`. Do not expose the orchestrator to anything you don't control without first turning on secure mode. + +## Secure mode (HMAC tokens) + +Set `ROBOCO_AGENT_AUTH_REQUIRED=true` to require a signed token on every REST request. In this mode: + +- `X-Agent-Token` becomes mandatory; a request without it is rejected with **401**. +- The token is an HMAC signed with `ROBOCO_AGENT_AUTH_SECRET` and **bound to the agent's id, role, and team**. The server recomputes the signature over the presented `X-Agent-ID` / `X-Agent-Role` / `X-Agent-Team` and compares it constant-time. If a caller swaps the role header to escalate to `ceo`, the signature no longer matches and the request is rejected with **401 — signature mismatch**. +- The orchestrator issues each agent its token at spawn time, so delivery agents are authenticated by construction. +- A presented token is **always** verified, even when auth isn't required — so you can roll out tokens before flipping the switch without breaking anything. + +| Flag | Default | Purpose | +|------|---------|---------| +| `ROBOCO_AGENT_AUTH_REQUIRED` | `false` | When `true`, REST requires a valid HMAC `X-Agent-Token`. | +| `ROBOCO_AGENT_AUTH_SECRET` | (unset) | Shared secret the orchestrator uses to sign and the API uses to verify the per-agent token. | + +!!! tip "How the panel authenticates as the CEO" + The control panel acts as the CEO agent. In secure mode, nginx injects the panel's CEO `X-Agent-Token` so your browser session is authenticated without you handling the secret — you just use the panel as normal. + +## The WebSocket caveat + +Token enforcement is **REST-only**. The [WebSocket streams](./websockets.md) do not check the HMAC token: + +- The per-resource sockets (`/ws/channels|agents|sessions|notifications/{id}`) validate their `agent_id`/`viewer_id` query param against the database and channel access, but not a token. +- `/ws/system` is fully unauthenticated. + +The streams are read-only and carry no control surface or secrets, so this isn't a privilege-escalation path the way the REST headers are — but it does mean the orchestrator port should stay trusted-network-only until WebSocket auth lands, even when you've enabled secure-mode REST. + +## What to do + +- **Single-host LAN, nothing untrusted on the network** → header-trust is fine; that's the default. +- **Anything reachable beyond a trusted LAN** → set `ROBOCO_AGENT_AUTH_REQUIRED=true` and a strong `ROBOCO_AGENT_AUTH_SECRET`, and keep the orchestrator port off the public internet regardless. + +For the full hardening checklist — network exposure, the GitHub PAT handling, and the prompt/bash guards — see [Security](../troubleshooting/security.md). + +## Next + +- [REST API](./rest-api.md) — the route surface these headers authorize. +- [WebSockets](./websockets.md) — the live streams and their separate auth model. diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 00000000..fd198a71 --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,25 @@ +# API Reference + +RoboCo is API-first: the control panel is just a client of the same REST and WebSocket surface you can drive yourself. This section is for integrators and the curious; the live, always-current schema is at **`/docs`** (Swagger UI) and **`/redoc`** on the orchestrator. + +
+ +- **[REST API](rest-api.md)** + + --- + + The `/api` domain routes, the agent gateway verbs, the error envelope, and where to find the live OpenAPI. + +- **[WebSocket streams](websockets.md)** + + --- + + The `/ws` live streams the panel consumes — per-resource feeds and the operator system stream. + +- **[Authentication](auth.md)** + + --- + + Header-trust mode versus secure token mode, and how the panel stays authenticated. + +
diff --git a/docs/api/rest-api.md b/docs/api/rest-api.md new file mode 100644 index 00000000..9534b089 --- /dev/null +++ b/docs/api/rest-api.md @@ -0,0 +1,119 @@ +# REST API + +RoboCo's backend is a single FastAPI application (`roboco/api/app.py`, one `create_app()` factory). It mounts every domain router under the `/api` prefix, the agent-gateway intent verbs under `/api/v1`, and the live WebSocket streams under `/ws`. Everything is fronted by nginx on `localhost:3000`, so the panel and any integrator use relative URLs (`/api/...`, `/ws/...`) against one origin with no CORS to configure. + +!!! tip "The live OpenAPI docs are the source of truth" + The fastest way to see the full, current REST surface — every path, request body, and response schema — is the interactive docs the app serves itself: + + - **Swagger UI** → [`http://localhost:3000/docs`](http://localhost:3000/docs) + - **ReDoc** → [`http://localhost:3000/redoc`](http://localhost:3000/redoc) + + This page is a map of *where things live*; `/docs` is the authoritative reference for *exactly how to call them*. + +## The two prefixes + +There are two distinct API surfaces, and the prefix tells you which one you're on: + +| Prefix | Audience | What it is | +|--------|----------|------------| +| `/api/*` | You / the panel / integrators | The domain REST surface — tasks, agents, projects, git, usage, settings, and the rest. This is what the control panel calls. | +| `/api/v1/flow/{role}/{verb}` and `/api/v1/do` | AI agents only | The [agent gateway](../company/agent-gateway.md). Agents never call the domain routes above — they POST intent verbs here through their MCP servers, and the server-side Choreographer enforces state, locks, and evidence. | + +Health and readiness probes sit at the root, not under `/api`: `GET /health` and `GET /ready`. + +## Domain route groups (`/api/*`) + +Every router is mounted under `/api`. The groups an operator or integrator hits: + +| Route group | Prefix | Purpose | +|-------------|--------|---------| +| Tasks | `/api/tasks` | The largest router: full task CRUD and lifecycle transitions (claim, submit, pass/fail, complete, escalate). The CEO god-mode override (`PATCH /api/tasks/{id}` with `X-Agent-Role: ceo`) lives here. | +| Kanban | `/api/kanban` | Board view of tasks grouped by state. | +| Agents | `/api/agents` | Agent roster, roles, teams, current state. | +| Work sessions | `/api/work-sessions` | Git session records — branch, commits, files, PR. | +| Projects | `/api/projects` | Repository config, CI/quality commands, git-token management. | +| Products | `/api/products` | Product entities + CEO approve-and-start / cell-routing. | +| Sessions | `/api/sessions` | Communication sessions and their messages. | +| Channels | `/api/channels` | Team channels. | +| Groups | `/api/groups` | Agent/channel grouping. | +| Messages | `/api/messages` | Extracted messages from agent streams. | +| Notifications | `/api/notifications` | Formal ack-required notifications. | +| Stream | `/api/stream` | Agent output stream access. | +| Journals | `/api/journals` | Agent journals and entries. | +| Optimal | `/api/optimal` | RAG queries (in-house pgvector engine). | +| Git | `/api/git` | Git operations surfaced for the panel. | +| Providers | `/api/providers` | Model-provider routing config. | +| Orchestrator | `/api/orchestrator` | Agent-runtime control (spawn/stop, dispatcher state). | +| Dashboard | `/api/dashboard` | Aggregated dashboard data. | +| Usage | `/api/usage` | Token/cost analytics (`GET /api/usage/summary?period=24h\|7d\|30d`). | +| System | `/api/system` | Rate-limit introspection (`GET /api/system/rate-limits`). | +| Settings | `/api/settings` | App settings, including feature-flag persistence. | +| Company goals | `/api/company-goals` | The company charter. | +| Cockpit | `/api/cockpit` | CEO read-only business summary. | +| Research | `/api/research` | Web-research subsystem (flag-gated). | +| Pitches | `/api/pitches` | Pitch provisioning (flag-gated). | +| Secretary | `/api/secretary` | Secretary chief-of-staff + its live-chat bridge. | +| Prompter | `/api/prompter` | Intake interviewer live chat (SSE relay). | +| Docs | `/api/docs` | Project documentation file management. | +| A2A | `/api/a2a` | Agent-to-agent messaging plumbing. | + +!!! info "Health vs readiness" + `GET /health` is a liveness probe — it returns 200 once the app is up. `GET /ready` is a readiness probe — it checks PostgreSQL and Redis and returns a `degraded` payload if either is down. Wire your uptime monitor to `/ready` if you want it to react to a backing-store outage, `/health` if you only care that the process is alive. See [Health & metrics](../operations/health-and-metrics.md). + +## The agent gateway (`/api/v1`) + +Agents do not touch the domain routes. They go through the gateway, which exposes one POST endpoint per (role, verb) pair plus a shared content-tools endpoint: + +| Endpoint | Role | +|----------|------| +| `POST /api/v1/flow/developer/{verb}` | Developer | +| `POST /api/v1/flow/qa/{verb}` | QA | +| `POST /api/v1/flow/documenter/{verb}` | Documenter | +| `POST /api/v1/flow/cell_pm/{verb}` | Cell PM | +| `POST /api/v1/flow/main_pm/{verb}` | Main PM | +| `POST /api/v1/flow/board/{verb}` | Board (Product Owner, Head of Marketing, Auditor) | +| `POST /api/v1/flow/auditor/{verb}` | Auditor | +| `POST /api/v1/flow/pr_reviewer/{verb}` | PR reviewer | +| `POST /api/v1/do` | Content tools (`commit`, `note`, `say`, `dm`, `evidence`) for every role | + +The `roboco-flow` MCP server in each agent container is a thin shim: it reads the agent's spawn manifest, registers only the verbs that role may call, and POSTs each one here. The verb set per role and the structural sandboxing are documented in [How agents are sandboxed](../company/agent-gateway.md) — you generally won't call these endpoints yourself. + +## The error envelope + +Every gateway verb returns the same standardized **envelope** (`roboco/services/gateway/envelope.py`), so agents recover from a rejection instead of looping: + +- **Success** carries `status`, `task_id`, `next` (the verb to call next), an optional `evidence` block, a `context_briefing`, and introspection fields (`current_state`, `valid_next_verbs`). +- **Error** carries an `error` flavor, a human `message`, a concrete `remediate` hint, and — for input gaps — a `missing` list and a `field_hints` answer-key. + +The error flavors: + +| `error` | Meaning | +|---------|---------| +| `tracing_gap` | A required tracing artifact (e.g. a commit, PR, or note) is missing. | +| `incomplete_input` | A required input field was not supplied; `missing` + `field_hints` tell the agent exactly which. | +| `invalid_state` | The verb isn't valid from the task's current state. | +| `not_authorized` | The role isn't allowed to perform this action. | +| `not_found` | The task or resource doesn't exist. | +| `circuit_open` | The agent has hammered one failing verb too many times; the breaker points it at a graceful exit. | + +The domain routes (`/api/*`) use FastAPI's standard error model, with the exception-handler stack in `roboco/api/middleware.py` mapping domain and service errors to clean responses. Notably: + +- A provider rate-limit error becomes **HTTP 429 with a `Retry-After` header**. +- A validation failure is a **422**, with a special remediation hint when an agent sends an 8-character short task id instead of a full UUID. +- Every response echoes back an `X-Correlation-ID` (and the request's `X-Response-Time-Ms`), so one id threads through the panel, the API, and the logs. + +## Quick checks + +```bash +curl -s localhost:3000/health +curl -s localhost:3000/ready +curl -s localhost:3000/api/system/rate-limits +curl -s 'localhost:3000/api/usage/summary?period=7d' +open http://localhost:3000/docs +``` + +## Next + +- [WebSockets](./websockets.md) — the live `/ws` streams the panel consumes. +- [Authentication](./auth.md) — header-trust vs. secure mode, and who can claim which role. +- [How agents are sandboxed](../company/agent-gateway.md) — the gateway, verbs, and envelope in depth. diff --git a/docs/api/websockets.md b/docs/api/websockets.md new file mode 100644 index 00000000..775b0477 --- /dev/null +++ b/docs/api/websockets.md @@ -0,0 +1,65 @@ +# WebSockets + +RoboCo pushes live updates over WebSocket endpoints under `/ws`, served by the orchestrator (`roboco/api/websocket.py`) and routed through nginx alongside the REST API. A single in-process `ConnectionManager` holds per-resource connection sets and broadcasts events to them. The panel consumes all of these through its `useWebSocket("/", …)` hook — you rarely connect to them directly, but they're the same streams an integrator can subscribe to. + +## The endpoints + +There are four per-resource streams plus one operator-wide stream: + +| Endpoint | Stream | Auth | +|----------|--------|------| +| `/ws/channels/{channel_id}` | Live messages in a team channel | `agent_id` query param, validated against the DB + channel access | +| `/ws/agents/{agent_id}` | An agent's output and lifecycle events | `viewer_id`/`agent_id` query param, validated against the DB | +| `/ws/sessions/{session_id}` | Messages in a communication session | `agent_id` query param, validated | +| `/ws/notifications/{agent_id}` | An agent's notifications | `agent_id` query param, validated | +| `/ws/system` | Operator/system-wide stream — no per-agent keying | **Unauthenticated, read-only** | + +All sockets support a `ping`/`pong` keepalive: send `{"type": "ping"}` and you'll get a `pong` back. + +!!! warning "WebSocket auth is not the REST auth" + The per-resource sockets validate their `agent_id`/`viewer_id` query param against the database (and channel access via the permissions layer), but they do **not** enforce the HMAC `X-Agent-Token` that secure-mode REST requires — token enforcement is REST-only. `/ws/system` is intentionally fully unauthenticated. None of the streams carry a control surface or secrets, so they're read-only by design, but the orchestrator port should be treated as trusted-network-only until WebSocket auth lands. See [Authentication](./auth.md) and [Security](../troubleshooting/security.md). + +## How events reach the sockets + +Server-side events are published to an in-process `StreamEventBus`. The bridge in `roboco/api/websocket_bridge.py` subscribes to it and registers a `_handle_*` forwarder per event type, mapping each `EventType` to the right socket broadcast. + +```mermaid +flowchart LR + S[Service / orchestrator] -->|publish EventType| B[StreamEventBus] + B --> WB[websocket_bridge _handle_*] + WB --> R[/ws/channels, /ws/agents, /ws/sessions, /ws/notifications/] + WB --> SYS[/ws/system/] + R --> P[Panel useWebSocket hook] + SYS --> P +``` + +To add a new live event you define an `EventType`, publish it to the bus, add a `_handle_*` forwarder in `websocket_bridge`, and consume it on the panel via the same hook — you never stand up a parallel endpoint. + +## Event types + +| Event | Arrives on | What it carries | +|-------|-----------|-----------------| +| `RATE_LIMIT_HIT` | `/ws/system` | A provider just hit a rate limit / overload and was parked. Drives the panel's amber rate-limit banner. | +| `RATE_LIMIT_LIFTED` | `/ws/system` | A parked provider recovered; queued work resumes. Clears the banner. | +| `USAGE_SNAPSHOT` | `/ws/system` | A fresh token-usage/cost snapshot. Drives the live "Token Usage & Cost" dashboard. | +| `NOTIFICATION_SENT` / `NOTIFICATION_ACKED` | `/ws/notifications/{agent_id}` | A notification was sent to or acknowledged by an agent. | +| `SESSION_CREATED` / `SESSION_CLOSED` / `SESSION_TIMEOUT` | `/ws/sessions/{session_id}` | Communication-session lifecycle. | +| `AGENT_SPAWNED` / `AGENT_STOPPED` / `AGENT_WAITING` / `AGENT_RESUMED` / `AGENT_ERROR` | `/ws/agents/{agent_id}` | Agent runtime lifecycle transitions. | + +Each forwarded message is a JSON object with a `type` field (the event-type name above) merged with the event's data. + +## REST fallbacks + +The two operator dashboards that ride `/ws/system` fall back to HTTP polling when the socket is down, so the panel keeps working without the stream: + +| Live event | HTTP fallback | +|------------|---------------| +| `RATE_LIMIT_HIT` / `RATE_LIMIT_LIFTED` | `GET /api/system/rate-limits` | +| `USAGE_SNAPSHOT` | `GET /api/usage/summary?period=24h\|7d\|30d` | + +See [Cost & usage](../operations/cost-and-usage.md) and [Health & metrics](../operations/health-and-metrics.md) for what the panel does with these. + +## Next + +- [REST API](./rest-api.md) — the `/api/*` route map and the error envelope. +- [Authentication](./auth.md) — the WebSocket-auth caveat in full. diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ac66224b1d96aa286cafeec9594f35321a43ec80 GIT binary patch literal 219657 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

2Wuoh~um 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ǯrQiI1N)BO2M_rx&4~UVvouqXmAXHx!<2 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 zß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 SGXMSix7C!i6)WVe!~Y+b4`_n` literal 0 HcmV?d00001 diff --git a/docs/company/agent-gateway.md b/docs/company/agent-gateway.md new file mode 100644 index 00000000..5dd9ecda --- /dev/null +++ b/docs/company/agent-gateway.md @@ -0,0 +1,49 @@ +# How agents are sandboxed + +The most important thing to understand about trusting RoboCo with your repository: **agents never touch your API, your database, or a shell directly.** Every action an agent can take goes through a narrow, server-side gateway that only exposes the handful of verbs that agent's *role* is allowed to use. Capability is decided by role at spawn time — not by the model's good behavior. + +## Agents speak in verbs, not API calls + +Each agent container talks to RoboCo through two thin MCP servers, both backed by a single server-side component (the **Choreographer**) that composes the real services behind the scenes: + +| Server | What it exposes | +|--------|-----------------| +| `roboco-flow` | **Intent verbs** — the lifecycle actions: `give_me_work`, `i_will_work_on`, `open_pr`, `i_am_done`, `claim_review`, `pass_review`, `complete`, `submit_up`, `submit_root`, `pr_pass`, … | +| `roboco-do` | **Content tools** — `commit`, `note`, `say`, `dm`, `evidence`. | + +Two more read-only servers give agents a read-only view of git (`status`, `log`, `diff`) and access to the knowledge base. That's the entire surface. There is no "run arbitrary SQL," no "call any endpoint," no general shell. + +## A role can only call its own verbs + +At spawn, every agent is handed a **manifest** listing exactly the verbs its role may call — and nothing else. The manifest is built from a server-side role configuration and mounted read-only into the container. The result is that the lifecycle's role rules aren't just policy, they're *unreachable code* for the wrong role: + +- A **developer** can `give_me_work`, open a PR, and mark itself done — but there is no merge verb in its manifest. +- **QA** can claim a review and pass or fail it — but it has no `commit`. +- A **PR reviewer** can pass or fail an assembled PR and post its review on the PR — but it never gets agent chat verbs. +- The **Auditor** is restricted to leaving a private note and reading evidence; it cannot `say` or `dm`. It observes; it does not participate. + +So when [the lifecycle](task-lifecycle.md#role-gated-transitions) says "only QA can pass QA" or "only the CEO merges to master," that boundary is enforced at the gateway: the verb simply isn't available to anyone else. + +## Every action returns a structured envelope + +Agents don't guess at state. Every verb returns a standardized **envelope**: + +- On success: `{ status, task_id, next, evidence?, context_briefing }` — where `next` tells the agent what to call next. +- On error: `{ error, message, remediate, missing }` — where `remediate` tells the agent exactly how to fix it and retry. + +That `next` / `remediate` contract is why agents move through the lifecycle reliably instead of flailing: the gateway leads them, step by step, and rejects anything out of order with an explanation rather than a crash. + +## The other guardrails + +A few more protections run by construction, the same way on every backend (Claude or Grok): + +- **Claim-locking** serializes work, so two agents can't grab the same task or race a merge. +- **The token never enters the container.** Your GitHub PAT is injected only for the moment of a git operation, orchestrator-side, and scrubbed from every clone — see [Register a project](../get-started/first-project.md#what-happens-under-the-hood). +- **A prompt-injection guard** screens task prompts, and a bash guard blocks credential-exfiltration and identity-forgery patterns. +- **Rate limits and overloads park, they don't crash-loop.** If a provider returns a 429 or a persistent overload, RoboCo *queues* that agent's work and probes for recovery instead of burning tokens retrying. You'll see an amber banner; the work resumes automatically when the provider does. + +The practical upshot for you as operator: the workforce is structurally constrained to do its job and only its job. You're not relying on twenty-five models all choosing to behave — you're relying on the fact that the misbehaving action isn't on the menu. + +## Next + +→ Watch it all in motion in [the Tour](../how-to/README.md), or head back to [the lifecycle](task-lifecycle.md). diff --git a/docs/company/index.md b/docs/company/index.md new file mode 100644 index 00000000..dda9fe2e --- /dev/null +++ b/docs/company/index.md @@ -0,0 +1,35 @@ +# The Company + +What keeps twenty-five agents from dissolving into noise is that RoboCo is relentlessly opinionated about *how* work happens. Everything is a task, no task moves without acceptance criteria, and every task walks the same strict, role-gated lifecycle — built, QA'd, documented, reviewed, approved. The structure is the point. It's what turns a roster of models into a company that actually ships. + +This section explains the machinery: + +

+ +If you'd rather see these ideas in motion than read about them, [the Tour](../how-to/README.md) follows one real feature through the whole company. diff --git a/docs/company/merge-model.md b/docs/company/merge-model.md new file mode 100644 index 00000000..1e6d5842 --- /dev/null +++ b/docs/company/merge-model.md @@ -0,0 +1,61 @@ +# The merge model + +A feature in RoboCo isn't one commit on one branch — it's a small tree of work that converges, as real pull requests, up a fixed chain to your repository's default branch. The rule at the top is simple and absolute: **only you ever merge to `master`.** + +## Branches, commits, and PRs are traceable + +Every branch, commit, and pull request carries the task ID it belongs to, so your git history reads back to the work that produced it. + +- **Branches** follow `{type}/{team}/{task-hierarchy}`, where the hierarchy uses `--` between levels (a `/` would collide with git's ref storage). Types are `feature`, `bug`, `chore`, `docs`, `hotfix`. + + ```text + feature/backend/ABC12345 # a root task + feature/backend/ABC12345--DEF67890 # a subtask + feature/backend/ABC12345--DEF67890--GHI11111 # a sub-subtask (max depth) + ``` + +- **Commits** are auto-prefixed with the short task ID: `[ABC12345] Add the auth endpoint`. +- **Pull requests** are titled the same way: `[ABC12345] `. + +A branch is created automatically the moment an agent claims a task, and a **work session** tracks its branch, base, commits, files changed, and pull request from claim to merge. + +## Work converges up a chain + +Each developer works in their **own clone** and opens a pull request from their branch. Those flow upward: + +```mermaid +graph BT + D1["dev branch"] --> C["cell branch"] + D2["dev branch"] --> C + C --> R["root / integration branch"] + C2["other cell branch"] --> R + R --> M["master"] + M:::ceo + classDef ceo fill:#1f6feb,color:#fff,stroke:#1f6feb; +``` + +1. **Developers → cell.** A cell's developers merge their work into the cell's branch. +2. **Cell → root.** The **cell PM** runs `submit_up` to open the cell → root pull request. The Main PM keeps one integration (root) branch per repository. +3. **Root → master.** The **Main PM** runs `submit_root` to open the final root → master pull request. + +Each of those assembled pull requests passes through the [in-path PR-review gate](task-lifecycle.md#the-in-path-pr-review-gate) before its PM merges it. + +## Only the CEO merges to master + +The final pull request — root → master — is the one place the company stops and hands the decision back to you. It lands in your **CEO Approval Queue** and waits. + +- The agent-facing merge path **hard-refuses to target the default branch.** A PM can merge a cell PR up to the root, but the merge to `master` is reserved for the CEO action, taken from `awaiting_ceo_approval`. +- **Force-push is CEO-only** too. + +From the queue you **Approve & Merge** (it ships to `master`), **Request Changes** (it loops back for another pass), or **Cancel**. This is the second of the only two moments the company needs you — the first being the green light that started the work. + +!!! info "Why a squash and one integration branch" + Cell pull requests are squash-merged, so each cell's work lands as a single verified commit on the integration branch, co-authored by the agent that wrote it. The final pull request then carries one clean commit per cell — three streams of work folded into one reviewable history. + +## Pull requests you didn't open + +Not every pull request comes from inside the company. When an external contributor or a fork opens one against your repository, the read-only **PR Reviewer** reads the diff against your standards and posts a single change-request on the PR — it never chats, merges, or decides. The PR then surfaces in the **PR Review Queue** on the Command Center, where you **Supersede** it (the company cuts its own branch from the contributor's commits, hardens it, opens its own PR, and links back to the original once that merges) or **Dismiss** it. Either way the call is yours, and the org never pushes to anyone else's fork. *(This inbound-review flow is feature-flagged; see the optional-subsystems reference.)* + +## Next + +→ **[How agents are sandboxed](agent-gateway.md)** — why a developer agent literally cannot perform the merge. diff --git a/docs/company/org-and-roles.md b/docs/company/org-and-roles.md new file mode 100644 index 00000000..2503273d --- /dev/null +++ b/docs/company/org-and-roles.md @@ -0,0 +1,92 @@ +# Org & roles + +RoboCo is **25 AI agents and one human — you, the CEO**. They're organized as a real company: a Board sets direction, a Main PM coordinates three delivery cells, and an Auditor watches everything. A few agents sit at your side on demand. You're on top of all of it. + +```mermaid +graph TD + CEO["👤 CEO — you (human)"] + + subgraph Support["At your side (on demand)"] + INTAKE["Intake<br/>drafts tasks with you"] + SEC["Secretary<br/>gated chief-of-staff"] + PRR["PR Reviewer<br/>inbound + root→master gate"] + end + + subgraph Board["Board"] + PO["Product Owner"] + HOM["Head of Marketing"] + AUD["Auditor (silent)"] + end + + MPM["Main PM<br/>coordinates the cells"] + + subgraph Cells["Delivery cells (6 agents each)"] + BE["Backend cell"] + FE["Frontend cell"] + UX["UX/UI cell"] + end + + CEO --- Support + CEO --> Board + Board --> MPM + MPM --> BE & FE & UX +``` + +## The cells + +The three delivery cells — **Backend, Frontend, UX/UI** — are where code gets written. Each cell is a small, complete team of **six agents**: + +| Role | Count | What they do | +|------|-------|--------------| +| **Cell PM** | 1 | Runs the cell like an engineering manager: delegates, clears blockers, triages, and folds the cell's work up to the Main PM. | +| **Developers** | 2 | Build the code in their own clones and open pull requests. | +| **QA** | 1 | Reads the real diff and decides whether work ships or comes back. Doesn't rubber-stamp. | +| **Documenter** | 1 | Writes down what was built, so the next agent — and you — don't start cold. | +| **PR Reviewer** | 1 | Reviews the cell's assembled pull request at the in-path gate before the PM merges it up. | + +UX/UI usually leads and sets the contracts; Frontend and Backend build against them. + +## The Board and the Main PM + +| Role | Reports to | What they do | +|------|-----------|--------------| +| **Product Owner** | CEO | Turns your ask into requirements and acceptance criteria. | +| **Head of Marketing** | CEO | Reviews work from the positioning / naming / user angle. | +| **Auditor** | CEO | Silent observer with read access to *everything*; reports quality concerns to you and never interferes. | +| **Main PM** | Board | Coordinates all three cells: fans a task out into per-cell subtasks, integrates the results, and opens the final pull request. | + +## At your side, on demand + +Three agents work directly with you rather than in the delivery flow. They run only while you're interacting with them or have given an explicit instruction: + +| Role | What they do | +|------|--------------| +| **Intake** | The conversational **Task Assistant** on the Prompter page. Reads your codebase and drafts a well-formed task with you. Chats only with you. | +| **Secretary** | Your conversational chief-of-staff. Reads the whole company's state to advise you and executes your directives — but every high-impact action is **gated** for your explicit confirmation. It spends nothing and approves nothing on its own. | +| **PR Reviewer** | The read-only main reviewer. Handles inbound external/fork pull requests and acts as the in-path gate on the final root → master pull request. It posts a review on the PR; it never chats, merges, or decides. | + +## The full roster + +25 agents, by their panel IDs: + +- **Backend:** `be-pm`, `be-dev-1`, `be-dev-2`, `be-qa`, `be-doc`, `be-pr-reviewer` +- **Frontend:** `fe-pm`, `fe-dev-1`, `fe-dev-2`, `fe-qa`, `fe-doc`, `fe-pr-reviewer` +- **UX/UI:** `ux-pm`, `ux-dev-1`, `ux-dev-2`, `ux-qa`, `ux-doc`, `ux-pr-reviewer` +- **Coordination:** `main-pm` +- **Board:** `product-owner`, `head-marketing`, `auditor` +- **At your side:** `intake-1`, `secretary-1`, `pr-reviewer-1` + +## How agents talk + +Communication is constant and logged. Agents narrate their reasoning, and formal **notifications** (the ones that need your acknowledgment) come only from PMs and the Board. Channels are seeded automatically: + +- **Cell channels** — `#backend-cell`, `#frontend-cell`, `#uxui-cell` +- **Cross-cell** — `#dev-all`, `#qa-all`, `#pm-all`, `#doc-all` +- **Management** — `#main-pm-board`, `#board-private` +- **Company-wide** — `#announcements` (read-only except Board / Main PM), `#all-hands` + +The **Auditor has silent read access to every channel.** You watch all of it from the **Communications** page. + +## Next + +→ **[The task lifecycle](task-lifecycle.md)** — the path every piece of work walks. diff --git a/docs/company/task-lifecycle.md b/docs/company/task-lifecycle.md new file mode 100644 index 00000000..ee0f255d --- /dev/null +++ b/docs/company/task-lifecycle.md @@ -0,0 +1,89 @@ +# The task lifecycle + +Everything in RoboCo is a task, and every task walks the same path. Each step is gated by role — only QA can pass QA, only the CEO can merge to `master` — so work can't skip a stage or land unreviewed. This is the backbone that makes the company trustworthy. + +```mermaid +stateDiagram-v2 + direction LR + [*] --> backlog + backlog --> pending: PM activates + pending --> claimed: agent claims + claimed --> in_progress: start work + in_progress --> verifying: self-verify + verifying --> awaiting_qa: submit (PR is open) + awaiting_qa --> awaiting_documentation: QA passes + awaiting_qa --> needs_revision: QA fails + needs_revision --> in_progress: rework + awaiting_documentation --> awaiting_pm_review: docs done + awaiting_pm_review --> completed: PM merges + awaiting_pm_review --> awaiting_ceo_approval: escalate + awaiting_ceo_approval --> completed: CEO approves & merges + awaiting_ceo_approval --> needs_revision: CEO requests changes + completed --> [*] + + in_progress --> blocked: external dependency + in_progress --> paused: temporarily stopped + blocked --> in_progress: unblocked + paused --> in_progress: resumed +``` + +## The states + +| State | What it means | Who owns the next move | +|-------|---------------|------------------------| +| `backlog` | PM setup phase — dependencies or session setup still needed. | PM | +| `pending` | Ready for work; the orchestrator can spawn an agent for it. | the matching role | +| `claimed` | An agent has locked the task. | the assignee | +| `in_progress` | Active development. | the assignee | +| `blocked` | An external dependency is blocking progress. | whoever clears it | +| `paused` | Temporarily stopped; can resume. | the assignee | +| `verifying` | The developer is self-verifying before handing off. | the developer | +| `awaiting_qa` | Submitted for QA — **a pull request is already open** so QA reviews the real diff. | QA | +| `needs_revision` | QA, a PR reviewer, or the CEO asked for changes. | the developer | +| `awaiting_documentation` | The Documenter writes up what was built (the PR is already open). | Documenter / Developer | +| `awaiting_pr_review` | The in-path PR-review gate: a reviewer checks an assembled pull request before the PM merges it. | PR reviewer | +| `awaiting_pm_review` | Docs are done; the PM reviews and merges. | PM | +| `awaiting_ceo_approval` | A major task escalated to you for the final call. | **you** | +| `completed` | Terminal — work done and merged. | — | +| `cancelled` | Terminal — work cancelled. | — | + +!!! note "The PR comes *before* QA" + A pull request is opened *before* QA review, not after. That lets QA read the actual PR diff on GitHub, and means the whole downstream approval chain — PM, then you — is signing off on a pull request that already exists. + +## When work is rejected + +Rejection isn't a dead end — it's a loop. When **QA fails** a task, or a **PR reviewer rejects** an assembled pull request, the task drops back to `needs_revision`, the developer reworks it, and it re-enters the flow. The same is true when *you* request changes from the CEO Approval Queue. Nothing is lost; the task carries its history, branch, and pull request with it the whole way around. + +## The in-path PR-review gate + +Most leaf developer tasks are reviewed by QA and never need a separate PR review. But when work is **assembled and pushed up the chain as a pull request**, it stops for a dedicated review before any PM merges it: + +```mermaid +stateDiagram-v2 + direction LR + in_progress --> awaiting_pr_review: submit_up / submit_root + awaiting_pr_review --> awaiting_pm_review: pr_pass + awaiting_pr_review --> needs_revision: pr_fail +``` + +- A **cell PM** runs `submit_up` to open the cell → root pull request. +- The **Main PM** runs `submit_root` to open the root → master pull request. +- Both land in `awaiting_pr_review`, where a PR reviewer either **`pr_pass`es** it on to the PM merge or **`pr_fail`s** it back to `needs_revision`. + +This gives the merge step a real reviewer with the power to reject — the one thing a PM otherwise lacks. **Leaf dev tasks and branchless coordination roots skip the gate.** + +## Role-gated transitions + +Transitions aren't suggestions; they're enforced. A handful of the rules: + +- **Activating** a task (`backlog → pending`) is PM-only. +- **Passing or failing QA** is QA-only, and a pass requires real review notes. +- **`pr_pass` / `pr_fail`** are PR-reviewer-only. +- **Merging** (`awaiting_pm_review → completed`) is PM-only; **escalating to the CEO** and the final **approve / request-changes / cancel** are CEO-only. +- **Cancelling** is PM-only. + +How those role boundaries are enforced — and why a developer literally cannot call the merge verb — is the subject of [How agents are sandboxed](agent-gateway.md). + +## Next + +→ **[The merge model](merge-model.md)** — how a task's branch travels up to `master`. diff --git a/docs/deploy/bootstrap-and-seeds.md b/docs/deploy/bootstrap-and-seeds.md new file mode 100644 index 00000000..edde170f --- /dev/null +++ b/docs/deploy/bootstrap-and-seeds.md @@ -0,0 +1,56 @@ +# Bootstrap & seeds + +A fresh RoboCo database isn't an empty shell — it comes pre-populated with the whole org chart: every agent, every communication channel, and the welcome messages that establish the channels. This page covers what `make db-init` seeds, and the console entry points that run the bootstrap. + +## `make db-init` — seed a fresh instance + +The one command you need to bring a clean database to life: + +```bash +make db-init +``` + +This runs `python -m roboco.cli --db-only`, which enables the schema (the [migration chain](./data-and-migrations.md)) and then seeds the company in a single transaction. + +!!! tip "It's idempotent — safe to re-run" + Every seed operation skips what already exists (agents and channels by slug; welcome messages are skipped if the channel already has any). Running `make db-init` against an already-seeded database is a harmless no-op, so you can re-run it any time without fear of duplicates. + +## What gets seeded + +| Seeded data | Detail | +|-------------|--------| +| **26 agent rows** | The 25 AI agents **plus the human CEO** ("Renzo"), each with a stable static UUID. A `system` sentinel row is also appended (it's the FK target for system-authored messages and lives outside the team enum). | +| **11 channels** | The 3 cell channels (`#backend-cell`, `#frontend-cell`, `#uxui-cell`), the 4 cross-role channels (`#dev-all`, `#qa-all`, `#pm-all`, `#doc-all`), and the management/special channels (`#main-pm-board`, `#board-private`, `#announcements`, `#all-hands`). | +| **Channel memberships** | Who can read and write each channel, plus the **Auditor's silent-observer access** to every channel. | +| **Welcome messages** | An opening message seeded into `#announcements`, `#all-hands`, and the three cell channels (each first spins up the backing group and session). | + +The roster and the seeded UUIDs are derived from the foundation catalog (`roboco/foundation/identity.py` for agents, `roboco/foundation/policy/communications.py` for channels), so the seed data and the runtime role/permission rules can never drift apart. For the full org chart and who sits in which channel, see [Org & roles](../company/org-and-roles.md). + +## The console entry points + +`make db-init` is a thin wrapper over the CLI. The supported way to invoke RoboCo directly is the module form: + +```bash +python -m roboco.cli --db-only # seed the DB and exit (what make db-init runs) +python -m roboco.cli # seed + start the full orchestrator stack +python -m roboco.cli --skip-db # start the stack against an already-seeded DB +python -m roboco.cli --spawn be-dev-1 fe-dev-1 # also spawn the named agents on boot +``` + +`python -m roboco.cli` is what the orchestrator container runs and what `make db-init` / `make` targets call. Full system start does more than seed: it brings up the Redis event bus, constructs the orchestrator, starts the API under uvicorn, polls `/health` until the FastAPI lifespan finishes indexing (up to ~2 minutes), then begins dispatching. + +!!! note "`python -m roboco.cli` is the working invocation" + `pyproject.toml` declares two console scripts, `roboco` and `roboco-bootstrap`. Use `python -m roboco.cli` (or `make db-init`) — that's the invocation the Makefile and the container use and the one that's verified to work. `roboco-bootstrap` maps to the same bootstrap routine; the bare `roboco` console script is not the supported path. + +## Required configuration before first boot + +Bootstrap itself needs almost nothing, but the orchestrator will refuse to start without two secrets: + +- **`ROBOCO_ENCRYPTION_KEY`** — the Fernet key that encrypts project GitHub tokens. Its config default is an empty string, but startup is mandatory: no key, no boot. +- **`ROBOCO_AGENT_AUTH_SECRET`** — the HMAC secret that signs agent tokens. Unset, every token is treated as unsigned. + +Everything else has a sensible default. See the [environment reference](./env-reference.md) for the full list, and [Deployment](./deployment.md) for the compose host-path mounts. + +## Next + +→ [Data & migrations](./data-and-migrations.md) — the entities you just seeded, and how the schema stays current. → [Org & roles](../company/org-and-roles.md) — the workforce these seeds create. diff --git a/docs/deploy/data-and-migrations.md b/docs/deploy/data-and-migrations.md new file mode 100644 index 00000000..d21b2fec --- /dev/null +++ b/docs/deploy/data-and-migrations.md @@ -0,0 +1,74 @@ +# Data & migrations + +RoboCo keeps all of its state in PostgreSQL. There is no separate document store, no schema you have to hand-build, and — in normal operation — no migration command you have to remember. This page gives you the operator's-eye view: the handful of entities worth understanding, the one hard database requirement (pgvector), and how the schema keeps itself up to date. + +## The entities you'll actually see + +The full data model spans about thirty Pydantic models in `roboco/models/` with matching SQLAlchemy tables in `roboco/db/tables.py`. You don't need most of them. These are the ones that show up across the panel and explain how work flows: + +| Entity | What it is | +|--------|------------| +| **Task** | The atomic unit of work. Carries its acceptance criteria, status (the 15-state [lifecycle](../company/task-lifecycle.md)), priority, branch name, PR number/URL, ownership (`created_by` / `assigned_to` / `team`), and its place in the tree (`parent_task_id`, `dependency_ids`, `blocker_ids`). A task points at **exactly one** of a `project_id` or a `product_id` — a single-repo task uses the project, a Board/fan-out coordination task uses the product. A validator enforces "one or the other, never both." | +| **Project** | A git repository configuration: `git_url`, default branch, protected branches, the Fernet-encrypted GitHub PAT (`git_token_encrypted` — the API only ever returns `has_git_token`, never the token), the per-project CI/gate commands, and the assigned cell. See [Register your first project](../get-started/first-project.md). | +| **WorkSession** | The link between an agent, a task, and a branch. One is created each time an agent claims a task. It tracks the base/target branches, the commits made, the files modified, the PR and merge outcome, and (when toolchain matching is on) the resolved Python version and toolchain status. | +| **Agent** | One row per member of the workforce — role, team, status — plus the human CEO. See [Org & roles](../company/org-and-roles.md). | +| **Session / Channel / Message** | The communication substrate. A `Channel` is a room; a `Session` is a live conversation inside it; a `Message` is one extracted line of an agent's stream. | +| **Notification** | A formal signal that requires acknowledgment (sent by PMs and the Board), distinct from the constant message stream. | +| **Journal** | Each agent's personal log of reflections and learnings, with `JournalEntry` rows underneath. | + +Two newer entities round out the company-in-a-box features: **Product** (with `ProductProject`, the per-cell repo-routing map for fan-out work) and **Pitch** (a Board proposal that the CEO approves and that can auto-provision a repo). You'll meet these on the [Business](../panel/business.md) page. + +## pgvector is required + +RoboCo's in-house RAG engine stores embeddings in PostgreSQL using the **pgvector** extension. The orchestrator enables it for you at startup (`CREATE EXTENSION IF NOT EXISTS vector`), so the bundled `postgres` image — which ships pgvector — works out of the box. + +!!! warning "If pgvector isn't installed" + On a bring-your-own PostgreSQL where the `vector` extension isn't available, the orchestrator logs a warning and **RAG features silently disable** — agents lose the knowledge base and mentor lookups, but the rest of the system runs. If you point RoboCo at an external Postgres, make sure pgvector is installed there. + +Operationally, **PostgreSQL and Redis are the only two stateful services to back up.** Everything the company knows lives in Postgres; Redis holds the event bus and short-lived coordination state. + +!!! danger "The encryption key is not in the database" + Each project's GitHub PAT is stored encrypted with `ROBOCO_ENCRYPTION_KEY`. A database backup **without that key is useless for the tokens** — you'll be able to restore every project except its credentials. Keep the key safe and separate. See the [environment reference](./env-reference.md). + +## The stack migrates itself on startup + +You almost never run a migration command by hand. When the orchestrator boots, `init_db()` enables pgvector and then `run_migrations()` runs `alembic upgrade head` in a worker thread: + +```mermaid +flowchart LR + A[Orchestrator starts] --> B[init_db: enable pgvector] + B --> C[run_migrations: alembic upgrade head] + C --> D[create_all gap-fill on existing DB] + D --> E[dispose pool to refresh enum cache] + E --> F[API ready] +``` + +The practical consequence: **a normal `docker compose up` / restart already applies any new migrations.** A fresh database is built entirely by the migration chain from base; an existing one runs only the pending steps (a pre-Alembic database is auto-stamped at the initial revision first, so it isn't re-built). + +!!! tip "The manual command is belt-and-suspenders" + The documented manual step — after pulling a change that adds a migration — + + ```bash + docker compose exec orchestrator alembic upgrade head + ``` + + is a safety net, not a routine requirement, since the orchestrator runs the same command on boot. In host-dev mode (no container) the equivalent is `uv run alembic upgrade head`, or `make migrate`. + +## A fresh DB is built by migrations, not `create_all` { #fresh-db-gotcha } + +This is the one gotcha that bites operators who try to reset state by hand. Several migrations embed **seed data** — most importantly the LLM provider rows (Anthropic, Ollama, self-hosted, Grok). The schema must be built by **running the migration chain**, never by a bare SQLAlchemy `create_all`. + +!!! warning "Don't reset a database with `create_all`" + A `create_all`-only database has empty `provider_configs`, so the Settings → Providers endpoints return **404**. The fix is always the same: let the migrations run (restart the orchestrator, or run `alembic upgrade head`). The orchestrator's own startup uses `create_all` only as a *gap-fill* on an already-migrated database, never as the builder. + +## Contributor notes + +If you write or review custom migrations: + +- **Revision-id length is capped at 32 characters.** PostgreSQL stores `alembic_version.version_num` as `VARCHAR(32)`. A longer id breaks a live `upgrade` even though the test suite (which renders offline / uses `create_all`) never catches it. The chain currently has **44 revisions** (45 files — revision `026` is split into two consecutive steps); the head is `044_convention_findings`, and the longest id in the tree is `015_drop_task_execution_outputs` at 31 characters, deliberately just under the limit. +- **The chain is linear.** Every revision has a single `down_revision`; there are no branches or merges to reconcile. +- **Models, tables, and migrations must stay in sync.** A Pydantic model in `roboco/models/`, its ORM table in `roboco/db/tables.py`, and the migration that creates the column are three layers that move together. + +## Next + +→ [Bootstrap & seeds](./bootstrap-and-seeds.md) — what `make db-init` puts into a fresh database. → [Environment reference](./env-reference.md) — every `ROBOCO_DATABASE_*` knob and the encryption key. diff --git a/docs/deploy/deployment.md b/docs/deploy/deployment.md new file mode 100644 index 00000000..c39b96dc --- /dev/null +++ b/docs/deploy/deployment.md @@ -0,0 +1,178 @@ +# Production deploy + +This is the operator reference for running RoboCo on a NAS or server. If you just want it up on your laptop, the [install quickstart](../get-started/installation.md) is faster — this page assumes you've done that once and now want the durable, server-side setup: the compose files, the host mounts agents need, where data lives, how to back it up, and how to harden it. + +!!! warning "Trusted network only" + RoboCo is built for a private LAN or homelab. Do not expose it directly to the public internet. nginx is the single entry point, but the orchestrator's WebSocket streams and (in header-trust mode) its API assume a trusted network. Put it behind your own VPN if you need remote access. + +## The three compose files + +There are **three tracked compose files**, and they are not interchangeable: + +| File | What it does | Needs a build toolchain? | +|------|--------------|--------------------------| +| `docker-compose.yml` | Builds every image from the Dockerfiles in `docker/`. | Yes | +| `docker-compose.yaml` | **Byte-identical** to `docker-compose.yml`. | Yes | +| `docker-compose.registry.yml` | Pulls and runs the **pre-built published images**. | No | + +`docker-compose.yml` and `docker-compose.yaml` are the same file under two names — Docker Compose picks up either, and the NAS deployment runs the `.yaml`. If you fork RoboCo and change a service, keep all three in sync. + +### Which one to run + +For a server you don't intend to hack on, run the **registry** file — it pulls finished images and needs no source tree or compiler on the host: + +```bash +docker compose -f docker-compose.registry.yml pull +docker compose -f docker-compose.registry.yml up -d +``` + +Two variables choose what you pull (defaults shown): + +```bash +ROBOCO_REGISTRY=ghcr.io/rennf93 # or docker.io/renzof93 +ROBOCO_VERSION=latest # or a pinned release, e.g. 0.8.0 +``` + +The orchestrator then spawns the **matching** pre-built agent images on demand (it reads `ROBOCO_AGENT_IMAGE_REGISTRY` / `ROBOCO_AGENT_IMAGE_TAG`, which the registry compose wires to the same registry and version). Pin `ROBOCO_VERSION` to a release tag in production so an upstream `latest` push can't silently change your fleet. + +Build from source only when you're modifying RoboCo: + +```bash +docker compose up -d # builds on first run +``` + +!!! note "Agent images are build/pull-only services" + The `agent-*-image` services in every compose file are one-shot stubs — they exist so `docker compose build`/`pull` materializes each per-role agent image up front. They never run as long-lived containers. The orchestrator spawns the actual agent containers itself, on demand, over the mounted Docker socket, and tears them down when their work is done. + +## The single origin + +nginx (`docker/nginx.conf`, rendered from an envsubst template) is the only externally-exposed service. It listens on `localhost:3000` and routes by path: + +```mermaid +flowchart LR + B[Browser :3000] --> N[nginx] + N -->|/| P[panel:3000] + N -->|/api/, /ws/, /health, /ready| O[orchestrator:8000] +``` + +| Path | Upstream | +|------|----------| +| `/api/`, `/ws/`, `/health`, `/ready` | `roboco-orchestrator:8000` | +| everything else | `roboco-panel:3000` | + +The browser only ever sees one origin (`:3000`), so there's no CORS to configure — the panel uses relative `/api` and `/ws` URLs and lets nginx dispatch. The panel container is never published directly; you reach it only through nginx. `/ws/` also gets a long (86400s) read timeout so live sockets stay open. + +The backing services *do* publish host ports for direct inspection — Postgres on **15432**, Redis on **16379**, Ollama on **11435**, and the orchestrator on **8000**. You don't route browser traffic at these; they're there for `psql`, `redis-cli`, and the like. + +## Required host-path mounts + +The orchestrator is Docker-in-Docker: it mounts `/var/run/docker.sock` and spawns agent containers itself. Because those agent bind-mounts resolve on the **host** daemon (not inside the orchestrator container), several paths must be given as **absolute host paths** — the orchestrator passes them straight through to `docker run -v` for each agent. + +| Variable | What it points at | Compose default | +|----------|-------------------|-----------------| +| `ROBOCO_HOST_PROJECT_DIR` | The RoboCo project directory on the host. | `/volume1/roboco` | +| `ROBOCO_HOST_CLAUDE_DIR` / `CLAUDE_AUTH_DIR` | The host `~/.claude` Claude Code auth dir, mounted into the orchestrator and each agent. | `/home/renzof/.claude` / `${HOME}/.claude` | +| `ROBOCO_HOST_DATA_DIR` | The host data dir handed to agents for shared volumes (workspaces, logs, grok-usage). | `/volume1/roboco/data` | +| `ROBOCO_DATA_DIR` | Host root for all persistent volumes mounted into the *backing* services and orchestrator (see below). | `./data` | +| `ROBOCO_HOST_GROK_DIR` | Host `~/.grok` SuperGrok auth — only needed if you run any agent on Grok. | `/home/renzof/.grok` | + +!!! danger "These must be real, absolute host paths" + A relative path or a path that only exists *inside* the orchestrator container will make agent spawns fail, because the host Docker daemon resolves the bind. On a NAS the project and data dirs usually live on the RAID volume (e.g. `/volume1/roboco` and `/volume1/roboco/data`). + +The host `~/.grok` is mounted **read-write** into the orchestrator (it rewrites the short-lived token in place to keep agents from hanging on an expired login) and **read-only** into each Grok agent. Run `grok login` on the host once before enabling Grok. Provider routing and the Grok runtime are covered in the models section. + +## Data persistence and backup + +Everything durable lives under `ROBOCO_DATA_DIR` (default `./data`). On a server, point this at a RAID volume: + +```bash +ROBOCO_DATA_DIR=/volume1/roboco/data +``` + +| Subdirectory | Holds | +|--------------|-------| +| `postgres/` | The entire database — tasks, projects, work sessions, journals, encrypted git tokens, the pgvector store. | +| `redis/` | Append-only cache, sessions, rate-limit + event-bus state. | +| `ollama/` | The local model cache (embedding model + local LLM) — large, but re-pullable. | +| `workspaces/` | Each agent's git clone of each project. | +| `logs/` | Per-agent run logs. | +| `mcp-configs/`, `prompts-generated/`, `agent-settings/`, `briefings/`, `manifests/` | Per-agent spawn artifacts the orchestrator writes. | +| `grok-usage/` | Per-agent Grok cost/usage capture. | + +For backup, the load-bearing directory is `postgres/` (everything that isn't re-derivable). `ollama/` and `workspaces/` are reconstructible — Ollama re-pulls models, agents re-clone repos — so they're optional in a backup. Take Postgres backups with `pg_dump` against the published port rather than copying the live data directory: + +```bash +pg_dump -h localhost -p 15432 -U roboco roboco > roboco-backup.sql +``` + +!!! danger "Back up `ROBOCO_ENCRYPTION_KEY` with the database" + Every per-project GitHub token in the database is Fernet-encrypted with `ROBOCO_ENCRYPTION_KEY`. **A database backup is useless without the key.** If you lose or change the key, every stored token becomes undecryptable and must be re-entered project by project. Store the key with your secrets, keep it stable across restarts, and never commit `.env`. + +## Secure mode + +On a trusted LAN RoboCo runs in **header-trust mode** by default (`ROBOCO_AGENT_AUTH_REQUIRED=false`): callers are identified by role headers, no token required. That's the intended homelab setup. + +To harden it so one agent can't spoof another's role, turn on fail-closed auth: + +```bash +ROBOCO_AGENT_AUTH_REQUIRED=true +ROBOCO_AGENT_AUTH_SECRET=<your HMAC secret> # already required for docker compose +ROBOCO_PANEL_AGENT_TOKEN=<from make panel-token> +``` + +With auth required, every API call must carry a valid `X-Agent-Token`. The panel runs in your browser and can't hold the signing secret, so nginx injects the CEO's token for it: generate the token with `make panel-token` (it signs one using your `ROBOCO_AGENT_AUTH_SECRET`), put it in `ROBOCO_PANEL_AGENT_TOKEN`, and nginx adds it as `X-Agent-Token` on `/api` and `/ws`. The panel keeps working; the secret never reaches the browser. + +`ROBOCO_ENCRYPTION_KEY` and `ROBOCO_AGENT_AUTH_SECRET` are both **required** for any docker compose run — the orchestrator service block guards them with compose `:?` so the stack refuses to start if either is unset. See [Security](../troubleshooting/security.md) for the full sandboxing model and [the env reference](./env-reference.md) for every knob. + +## Startup sequence + +`depends_on` conditions enforce a strict boot order; the effective sequence is: + +```mermaid +flowchart LR + PG[postgres] --> OL[ollama] + RD[redis] --> OL + OL --> OI[ollama-init] + OI --> OR[orchestrator] + AB[agent-base-image] --> OR + OR --> PN[panel] + PN --> NG[nginx] + OR --> NG +``` + +- **postgres / redis / ollama** must each pass their healthcheck (`pg_isready`, `redis-cli ping`, `ollama list`) before anything downstream starts. +- **ollama-init** is a one-shot that best-effort pulls the embedding model and the local LLM, then gates success on the models being **present** — a degraded model registry can't take down a fully-cached deployment. +- **orchestrator** waits for postgres + redis + ollama healthy, ollama-init completed, and agent-base-image built. On startup it **runs the database migrations itself** (idempotently, to head) and indexes its knowledge base — you do not run `alembic upgrade head` by hand for the compose path. +- **panel** waits for the orchestrator; **nginx** waits for both. + +First boot is the slow one: the model pulls (the LLM is a couple of minutes) plus knowledge-base indexing. Watch it come up: + +```bash +docker compose logs -f orchestrator +curl http://localhost:8000/health +docker ps --filter name=roboco +``` + +When the orchestrator reports serving, open `http://localhost:3000`. A boot that hangs is almost always waiting on `ollama-init` (model pull) or a healthcheck — check `docker compose ps` to see which service is still `starting`. Migration and data details are in [Data & migrations](./data-and-migrations.md); recurring boot symptoms are in [Common issues](../troubleshooting/common-issues.md). + +## Operator-relevant Makefile targets + +The `Makefile` drives the **host** developer workflow (uv-based, for hacking on RoboCo itself) — it is separate from the Docker stack and needs `uv` on the host. The handful that matter operationally: + +| Target | Does | +|--------|------| +| `make panel-token` | Prints a signed CEO token for `ROBOCO_PANEL_AGENT_TOKEN` (secure mode). | +| `make infra` | Brings up only postgres + redis (`make infra-down` stops them) — for host-side dev against the backing services. | +| `make migrate` | Runs `alembic upgrade head` on the host (the compose stack self-migrates; this is the host-dev path). | +| `make run` | Runs the API + orchestrator on the host (no `--reload`); `make api` is the reload dev server, `make dev` runs both. | +| `make quality` | The full merge gate: ruff format-check + lint, mypy, pytest with 80% coverage floor, complexity, security, dependency, and migration checks. | +| `make serve-docs` | Serves this documentation locally with `mkdocs serve`. | +| `make status` / `make logs` | Orchestrator status / recent logs against a running instance. | + +Run `make help` for the full list. + +## Next + +- **[Environment reference](./env-reference.md)** — every `ROBOCO_*` setting, with defaults and on/off state. +- **[Data & migrations](./data-and-migrations.md)** — the self-migrating schema and what to back up. +- **[Security](../troubleshooting/security.md)** — the full agent sandboxing and auth model. diff --git a/docs/deploy/env-reference.md b/docs/deploy/env-reference.md new file mode 100644 index 00000000..f138f5e7 --- /dev/null +++ b/docs/deploy/env-reference.md @@ -0,0 +1,248 @@ +# Environment reference + +This is the canonical list of every `ROBOCO_*` setting. They are all read by a single Pydantic-Settings class (`roboco/config.py`), loaded from the process environment and `.env`, prefixed with `ROBOCO_`, and case-insensitive. Most have a working default; the few that don't, and the ones the orchestrator refuses to start without, are flagged below. + +!!! tip "You rarely set most of these" + For a working deploy you set the two required secrets, the host paths, and maybe a feature flag or two. The long tables here exist so that when you *do* need to tune a timeout or a window, you can find it. The defaults shown are RoboCo's config defaults; a few compose-only defaults differ and are called out. + +A feature flag set in `.env` takes effect on the next backend restart. The env-gated subsystems can also be toggled from the panel's **Settings → Feature Flags** card, which persists to the settings store and overrides the env default; an unset toggle falls back to the env/config default. See the [Optional capabilities](../optional/index.md) section for what each subsystem does. + +## Required secrets + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_ENCRYPTION_KEY` | *(empty — required)* | Fernet key encrypting every per-project git token at rest. The orchestrator **refuses to start** without it (compose `:?` guard). Generate with `python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())'`. Keep it stable — losing it makes stored tokens undecryptable. | +| `ROBOCO_AGENT_AUTH_SECRET` | *(empty — required for compose)* | HMAC secret signing the per-agent `X-Agent-Token`. Generate with `python -c 'import secrets; print(secrets.token_hex(32))'`. | + +## Security & auth + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_AGENT_AUTH_REQUIRED` | `false` | Fail-closed secure mode. When `true`, every API call must carry a valid token. Requires `ROBOCO_PANEL_AGENT_TOKEN` to keep the panel working. On a trusted LAN, leave `false` (header-trust mode). | +| `ROBOCO_PANEL_AGENT_TOKEN` | *(empty)* | The CEO token nginx injects as `X-Agent-Token` on `/api` and `/ws` in secure mode, so the panel works without the browser holding the signing secret. Generate with `make panel-token`. | + +## Application & API server + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_APP_VERSION` | `0.8.0` | Reported app version. | +| `ROBOCO_DEBUG` | `false` | Debug mode. | +| `ROBOCO_ENVIRONMENT` | `development` | One of `development` / `staging` / `production`. Selects the JSON log renderer (prod) vs console renderer. The compose stack sets `production`. | +| `ROBOCO_HOST` | `127.0.0.1` | Bind address. Use `0.0.0.0` in containers. | +| `ROBOCO_PORT` | `8000` | API port. | +| `ROBOCO_API_URL` | *(unset)* | Override base URL for containerized agents (e.g. `http://roboco-orchestrator:8000`); otherwise built from host/port. | +| `ROBOCO_CORS_ORIGINS` | `["http://localhost:3000","http://localhost:5173"]` | Allowed CORS origins. The single-origin nginx setup means you rarely change this. | +| `ROBOCO_CORS_ALLOW_CREDENTIALS` | `true` | Whether CORS allows credentials. | +| `ROBOCO_PUBLIC_BASE_URL` | `http://127.0.0.1:8000` | Reachable base URL embedded in commit-trailer links — set to your LAN IP or domain so the links resolve. | + +## Database + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_DATABASE_HOST` | `localhost` | Postgres host (`roboco-postgres` in compose). | +| `ROBOCO_DATABASE_PORT` | `5432` | Postgres port. | +| `ROBOCO_DATABASE_USER` | `roboco` | Postgres user. | +| `ROBOCO_DATABASE_PASSWORD` | `roboco` | Postgres password — change it for any real deployment. | +| `ROBOCO_DATABASE_NAME` | `roboco` | Database name. | +| `ROBOCO_DATABASE_ECHO` | `false` | Log every SQL statement. | +| `ROBOCO_DATABASE_POOL_SIZE` | `10` | Connection pool size. | +| `ROBOCO_DATABASE_MAX_OVERFLOW` | `20` | Extra connections beyond the pool. | +| `ROBOCO_DATABASE_POOL_TIMEOUT` | `10` | Seconds to wait for a pooled connection. | +| `ROBOCO_DATABASE_POOL_RECYCLE` | `1800` | Recycle a connection after this many seconds. | + +## Redis + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_REDIS_HOST` | `localhost` | Redis host (`roboco-redis` in compose). | +| `ROBOCO_REDIS_PORT` | `6379` | Redis port. | +| `ROBOCO_REDIS_DB` | `0` | Redis logical DB. | +| `ROBOCO_REDIS_PASSWORD` | *(unset)* | Optional Redis password. | + +## RAG, embeddings & Ollama + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_RAG_PERSIST_DIR` | `.roboco` | Local RAG persistence dir. | +| `ROBOCO_RAG_CHUNK_STRATEGY` | `fixed` | One of `fixed` / `semantic` / `hierarchical` / `contextual`. `fixed` recommended; `semantic` loads an extra model. | +| `ROBOCO_RAG_CHUNK_SIZE` | `512` | Base chunk size. | +| `ROBOCO_RAG_CHUNK_SIZE_DOCS` | `1536` | Chunk size for docs. | +| `ROBOCO_RAG_CHUNK_SIZE_JOURNALS` | `1024` | Chunk size for journals/reflections. | +| `ROBOCO_RAG_CHUNK_OVERLAP` | `128` | Chunk overlap. | +| `ROBOCO_RAG_AUTO_UPDATE_ENABLED` | `true` | Whether the RAG index auto-refreshes. | +| `ROBOCO_RAG_AUTO_UPDATE_INTERVAL` | `300` | Seconds between auto-updates. | +| `ROBOCO_ANTHROPIC_API_KEY` | *(unset)* | Optional Anthropic key. Agents use the mounted Claude Code auth, not a metered key. | +| `ROBOCO_DEFAULT_EMBEDDING_MODEL` | `qwen3-embedding:0.6b` | Embedding model (1024-dim). | +| `ROBOCO_EMBEDDING_DIMENSIONS` | `1024` | Embedding dimensions — must match the model. | +| `ROBOCO_LOCAL_LLM_MODEL` | `glm-5:cloud` | Local LLM for RAG answer synthesis. | +| `ROBOCO_LOCAL_LLM_BASE_URL` | `http://roboco-ollama:11434/v1` | Ollama OpenAI-compatible endpoint. | +| `ROBOCO_OLLAMA_BASE_URL` | `http://roboco-ollama:11434` | Ollama native endpoint (embeddings, model management). | + +## Workspaces & git timeouts + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_WORKSPACES_ROOT` | `/data/workspaces` | Root for all agent git clones. | +| `ROBOCO_WORKSPACE_AUTO_CLONE` | `true` | Auto-clone a repo on first workspace access. | +| `ROBOCO_WORKSPACE_CLONE_TIMEOUT` | `300` | Seconds for a `git clone`. | +| `ROBOCO_WORKSPACE_REFRESH_FETCH_TIMEOUT_SECONDS` | `60` | Timeout for the best-effort `git fetch` on re-entry into a healthy clone. | +| `ROBOCO_WORKSPACE_INSTALL_DEV_DEPS` | `true` | After cloning, install the project's dev dependencies into the workspace so `make quality` runs without re-downloading tooling. | +| `ROBOCO_WORKSPACE_DEP_INSTALL_TIMEOUT_SECONDS` | `600` | Timeout for that post-clone dependency install. | +| `ROBOCO_GIT_COMMAND_TIMEOUT_SECONDS` | `30` | Timeout for a single local git subprocess (status, log, checkout). | +| `ROBOCO_GIT_COMMIT_TIMEOUT_SECONDS` | `180` | Timeout for staging + committing a changeset. | +| `ROBOCO_GIT_NETWORK_TIMEOUT_SECONDS` | `120` | Timeout for git ops that talk to origin (fetch / pull / push). | +| `ROBOCO_PROTECTED_GIT_URLS` | *(empty)* | Repo URL substrings a project may not point at — blocks agent commits/merges from reaching a protected repo. | +| `ROBOCO_SESSION_IDLE_TIMEOUT_SECONDS` | `3600` | Idle seconds before a messaging session is swept closed. | + +## Agent images (spawn source) + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_AGENT_IMAGE_REGISTRY` | *(empty)* | Registry namespace for pre-built agent images (e.g. `ghcr.io/rennf93`). Empty = build locally. The registry compose wires this to `ROBOCO_REGISTRY`. | +| `ROBOCO_AGENT_IMAGE_TAG` | *(empty)* | Tag for pre-built agent images (e.g. `0.8.0`). Empty = implicit `:latest`. The registry compose wires this to `ROBOCO_VERSION`. | + +!!! note "Deploy-time variables (compose, not config.py)" + A few variables are consumed by the compose files and host-mount wiring rather than by `config.py`: `ROBOCO_REGISTRY`, `ROBOCO_VERSION`, `ROBOCO_DATA_DIR`, `ROBOCO_HOST_PROJECT_DIR`, `ROBOCO_HOST_CLAUDE_DIR` / `CLAUDE_AUTH_DIR`, `ROBOCO_HOST_DATA_DIR`, and `ROBOCO_HOST_GROK_DIR`. They are documented in the [production deploy reference](./deployment.md#required-host-path-mounts). + +## Transcript retention + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_TRANSCRIPT_RETENTION_DAYS` | `14` | Days to keep agent Claude Code transcripts. A stored panel setting overrides this default. | +| `ROBOCO_TRANSCRIPT_PRUNE_ENABLED` | `true` | Whether the background sweep prunes old transcripts. | +| `ROBOCO_TRANSCRIPT_PRUNE_INTERVAL_SECONDS` | `3600` | Minimum seconds between prune passes. | + +## Spawn pacing, SLAs & reaper windows + +The orchestrator's dispatcher uses these to pace spawns, detect loops, and reclaim stuck work. Defaults are tuned for real LLM latency — raise the reaper windows (not lower) if long agent tasks are being reaped mid-work. + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_AGENT_TOOL_CALL_WARN` | `50` | Soft warning threshold for per-session tool calls. | +| `ROBOCO_AGENT_TOOL_CALL_HALT` | `150` | Hard cap on per-session tool calls; the orchestrator stops the container. | +| `ROBOCO_AGENT_LOOP_THRESHOLD` | `3` | Identical tool+args repeats in the window that flag a loop. | +| `ROBOCO_AGENT_LOOP_WINDOW` | `10` | How many recent tool calls to inspect for loop detection. | +| `ROBOCO_AGENT_STOP_ATTEMPT_ALLOWANCE` | `1` | Stop-without-terminal attempts before auto-substitute. | +| `ROBOCO_AGENT_SLA_DEVELOPER_IN_PROGRESS` | `7200` | SLA (s) for a developer in `in_progress`. | +| `ROBOCO_AGENT_SLA_DEVELOPER_VERIFYING` | `1800` | SLA (s) for a developer in `verifying`. | +| `ROBOCO_AGENT_SLA_QA_CLAIMED` | `1800` | SLA (s) for QA on a claimed review. | +| `ROBOCO_AGENT_SLA_DOCUMENTER_CLAIMED` | `3600` | SLA (s) for a documenter on a claimed task. | +| `ROBOCO_AGENT_SLA_CELL_PM_CLAIMED` | `14400` | SLA (s) for a cell PM on a claimed task. | +| `ROBOCO_CLAIM_STALE_SECONDS` | `180` | Claim-heartbeat staleness used by the spawn trigger filter. | +| `ROBOCO_STALE_CLAIM_REAP_SECONDS` | `600` | Reaper-only stale-claim threshold before releasing a claim back to pending. | +| `ROBOCO_PM_CLOSURE_RECENTLY_PAUSED_SECONDS` | `45` | Debounce before respawning a PM to close a recently paused parent. | +| `ROBOCO_GROK_IDLE_KILL_SECONDS` | `900` | Idle-container kill threshold for Grok agents (they emit no SDK heartbeat). | +| `ROBOCO_GROK_MAX_COST_USD` | `0.0` | Per-agent Grok cost ceiling (USD) before kill; `0` disables. | +| `ROBOCO_INTERACTIVE_IDLE_REAP_SECONDS` | `1800` | Idle-reap threshold for live intake/secretary chats; `0` disables. | +| `ROBOCO_CLAIMED_NO_AGENT_GRACE_SECONDS` | `120` | Grace window before respawning/releasing a claimed task with no running agent. | +| `ROBOCO_PM_DECISION_WINDOW_SECONDS` | `300` | Recency window for a PM `journal:decision` to satisfy gating verbs. | +| `ROBOCO_SPAWN_COOLDOWN_SECONDS` | `60` | Per-task spawn-rate cooldown. | +| `ROBOCO_ROLE_SPAWN_RATE_PER_MINUTE` | `6` | Per-role spawn-rate limit per minute. | + +## Gateway: manifests & tracing-gate minimums + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_MANIFEST_HOST_DIR` | `/app/manifests` | Orchestrator dir where per-agent tool manifests are written; must be a host-bind-mounted path so the daemon can mount each manifest into its agent. | +| `ROBOCO_QA_NOTES_MIN_CHARS` | `80` | Minimum characters for QA notes. | +| `ROBOCO_DOCS_NOTES_MIN_CHARS` | `20` | Minimum characters for docs notes. | +| `ROBOCO_DEV_NOTES_MIN_CHARS` | `40` | Minimum characters for a developer's `dev_notes`. | +| `ROBOCO_PR_REVIEWER_NOTES_MIN_CHARS` | `40` | Minimum characters for a PR reviewer's notes. | +| `ROBOCO_QUICK_CONTEXT_MIN_CHARS` | `30` | Minimum characters for a PM's `quick_context` resumption section. | +| `ROBOCO_COMMIT_SUBJECT_MIN_CHARS` | `20` | Minimum characters for a commit subject. | +| `ROBOCO_COMMIT_BANNED_WORDS` | `wip,tmp,asdf,oops,fix,update,change,stuff,things` | Banned single-word commit subjects. | + +## Grok runtime + +Only relevant if you run any agent on Grok. See the [models section](../models/grok.md) for the full runtime. + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_HOST_GROK_DIR` | `/home/renzof/.grok` (compose) | Host `~/.grok` SuperGrok auth dir; mounted read-write into the orchestrator (token auto-refresh) and read-only into Grok agents. The same value is both the source and target path. | +| `ROBOCO_GROK_AGENT_IMAGE` | `roboco-agent-grok:latest` | Image the orchestrator spawns for Grok agents. | +| `ROBOCO_GROK_CLI_MODEL` | `grok-build` | Grok CLI model id. | +| `ROBOCO_GROK_REASONING_EFFORT` | *(empty)* | `low`/`medium`/`high`/`xhigh`/`max` for all Grok agents; empty keeps the model default. | +| `ROBOCO_GROK_MAX_TURNS` | `200` | Hard ceiling on agentic turns per Grok run (loop guard). | +| `ROBOCO_GROK_IDLE_KILL_SECONDS` | `900` | (see reaper table) Idle-kill window for a wedged Grok container. | +| `ROBOCO_GROK_MAX_COST_USD` | `0.0` | (see reaper table) Per-agent Grok cost ceiling. | + +## Optional subsystem flags (default-off unless noted) + +These gate the env-toggled capabilities. Each is inert when off. See [Optional capabilities](../optional/index.md). + +### Web research — default **on** + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_RESEARCH_ENABLED` | `true` | Master switch for web research. When `false`, the search MCP is not mounted into any agent. | +| `ROBOCO_RESEARCH_PROVIDER` | `tavily` | `tavily` / `brave` / `exa` / `null`. | +| `ROBOCO_RESEARCH_API_KEY` | *(unset)* | Provider key — **server-side only**, never reaches an agent. Unset = empty-result null provider. | +| `ROBOCO_RESEARCH_MAX_RESULTS` | `5` | Cap on results per search (1–20). | +| `ROBOCO_RESEARCH_FETCH_MAX_CHARS` | `20000` | Cap on extracted characters per fetch. | +| `ROBOCO_RESEARCH_TIMEOUT_SECONDS` | `15.0` | Per-request outbound timeout. | +| `ROBOCO_RESEARCH_DAILY_QUOTA_PER_AGENT` | `50` | Search+fetch calls per agent per UTC day. | + +### GitHub repo provisioning — default **on** (inert without token/org) + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_PROVISIONING_ENABLED` | `true` | Master switch for pitch auto-provisioning. Inert with no token/org regardless. | +| `ROBOCO_PROVISIONING_TOKEN` | *(empty)* | GitHub PAT (repo + org admin) used to create repos — server-side only. | +| `ROBOCO_PROVISIONING_ORG` | *(empty)* | GitHub org where new repos are created. | +| `ROBOCO_GITHUB_API_BASE_URL` | `https://api.github.com` | Override for GitHub Enterprise. | +| `ROBOCO_PROVISIONING_TIMEOUT_SECONDS` | `30.0` | Per-request provisioning timeout. | +| `ROBOCO_PROVISIONING_REPO_PRIVATE` | `true` | Whether provisioned repos are private. | + +### Architectural conventions — default **off** + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_CONVENTIONS_ENABLED` | `false` | Master switch for the per-project conventions standard (scaffold, ambient injection, baseline constraints, gate enforcement). Fully inert when off. | + +### Toolchain matching — default **off** + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_TOOLCHAIN_MATCH_ENABLED` | `false` (config) / `true` (compose) | Provision the agent workspace with the target project's Python and block delivery gates when the suite can't run. The compose orchestrator block defaults this **on**. | + +### Provider overload break — default **on** + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_OVERLOAD_BREAK_ENABLED` | `true` | Park a provider on a persistent overload (HTTP 529/500/503) the way a 429 is parked, instead of crash-retrying. | + +### Strategy engine — default **off** + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_STRATEGY_ENGINE_ENABLED` | `false` | Master switch for the autonomous strategy engine (notify-only). When off the loop never runs. | +| `ROBOCO_STRATEGY_ENGINE_INTERVAL_SECONDS` | `1800` | Seconds between assessment passes. | +| `ROBOCO_STRATEGY_STRANDED_BLOCKED_MINUTES` | `120` | A task blocked longer than this is surfaced as stranded. | + +### External / internal PR review — default **off** + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_EXTERNAL_PR_ENABLED` | `false` (config) / `true` (compose) | Master switch for inbound external/fork PR review. The compose orchestrator block defaults this on. | +| `ROBOCO_EXTERNAL_PR_POLL_INTERVAL_SECONDS` | `300` | Seconds between inbound external-PR discovery passes. | +| `ROBOCO_EXTERNAL_PR_AUTHOR_ALLOWLIST` | *(empty)* | GitHub usernames auto-trusted. Empty = every external PR needs human confirmation. | +| `ROBOCO_EXTERNAL_PR_REQUIRE_HUMAN_CONFIRM` | `true` | Require explicit human confirmation before any agent fetches/checks-out/executes external code. | +| `ROBOCO_INTERNAL_PR_ENABLED` | `false` | Also review org-repo (non-fork) PRs not tied to an active task. | + +### Self-healing CI loop — default **off** + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROBOCO_SELF_HEAL_ENABLED` | `false` | Master switch for the self-heal loop (detect + notify the CEO). When off the loop never runs. | +| `ROBOCO_SELF_HEAL_PROJECT_SLUG` | *(empty)* / `roboco-api` (compose) | The registered project that *is* RoboCo itself — the only repo the loop watches/originates into. | +| `ROBOCO_SELF_HEAL_CI_WORKFLOW` | `ci.yml` | GitHub Actions workflow file to scope the CI signal to. | +| `ROBOCO_SELF_HEAL_ORIGINATE_ENABLED` | `false` | Second opt-in: on a regression, also open a PENDING fix task. The loop never approves, merges, or deploys. | +| `ROBOCO_SELF_HEAL_INTERVAL_SECONDS` | `1800` | Seconds between telemetry passes. | +| `ROBOCO_SELF_HEAL_MAX_OPEN_TASKS` | `3` | Rolling cap on concurrently-open self-heal tasks. | +| `ROBOCO_SELF_HEAL_MAX_PER_CYCLE` | `1` | Max self-heal tasks originated in one cycle. | + +## Next + +- **[Production deploy](./deployment.md)** — compose files, host mounts, secure mode, startup. +- **[Optional capabilities](../optional/index.md)** — what each flag above turns on. +- **[Settings panel](../panel/settings.md)** — toggling flags from the UI instead of `.env`. diff --git a/docs/deploy/index.md b/docs/deploy/index.md new file mode 100644 index 00000000..96f665ad --- /dev/null +++ b/docs/deploy/index.md @@ -0,0 +1,31 @@ +# Configure & Deploy + +The reference layer beneath [Get Started](../get-started/index.md): the full configuration surface, the production deployment story, and how RoboCo's data and schema are managed. + +<div class="grid cards" markdown> + +- **[Deployment](deployment.md)** + + --- + + The compose files, the single-origin nginx, the host-path mounts, data persistence and backup, secure mode, and the startup sequence. + +- **[Environment reference](env-reference.md)** + + --- + + Every `ROBOCO_*` setting, by category, with its default and purpose — the canonical configuration reference. + +- **[Data & migrations](data-and-migrations.md)** + + --- + + The core data model, the pgvector requirement, and how the stack migrates itself on every boot. + +- **[Bootstrap & seeds](bootstrap-and-seeds.md)** + + --- + + What `make db-init` seeds — the agents, channels, and memberships a fresh company starts with. + +</div> diff --git a/docs/get-started/first-project.md b/docs/get-started/first-project.md new file mode 100644 index 00000000..788f3e3e --- /dev/null +++ b/docs/get-started/first-project.md @@ -0,0 +1,57 @@ +# Register your first project + +A **project** is a git repository RoboCo is allowed to work on, plus the configuration that tells the company how to build and check it. Until you register one, the agents have nowhere to put their work. You register projects in the panel under **Projects → New**. + +## What a project needs + +| Field | What it is | +|-------|-----------| +| **Name** | Human-readable label for the repo. | +| **Slug** | URL-safe short name. It becomes the top of the workspace path (`{slug}/{team}/{agent}/`) and shows up in branch names. | +| **Git URL** | The clone URL. HTTPS is the common case and **requires a token** (below). | +| **GitHub token (PAT)** | A Personal Access Token, stored encrypted. Required for private or HTTPS repos — see [The GitHub token](#the-github-token). | +| **Assigned cell** | Which delivery cell owns this repo: Backend, Frontend, or UX/UI. | +| **Default branch** | The branch PRs ultimately target. **Read [the default-branch gotcha](#the-default-branch-gotcha) before you save.** | +| **Gate commands** *(optional)* | Per-project test / lint / format / typecheck / build commands, and a fast pre-submit `quality_command`. See [Gate commands](#gate-commands). | + +## The GitHub token + +Agents clone your repository and open pull requests on it, so they need a **GitHub Personal Access Token** with permission to do that. + +- **Scopes:** the token needs repository **contents** access (to clone and push branches) and **pull request** access (to open and merge PRs). A classic `repo`-scoped token works; a fine-grained token needs *Contents: Read and write* and *Pull requests: Read and write* on the target repo. +- **It's encrypted and write-only.** The token is encrypted at rest the moment you save it (with your `ROBOCO_ENCRYPTION_KEY`) and the API **never returns it** — the panel only shows whether a token is set, not its value. +- **Rotating or clearing it:** in **Edit Project**, entering a new token replaces it, an empty field clears it, and leaving it untouched keeps the current one. + +!!! danger "HTTPS without a token fails" + If you give an HTTPS Git URL and no token, the clone fails — agents can't reach the repo. Set the token when you create the project. + +!!! warning "The encryption key is load-bearing" + Every stored project token is encrypted with `ROBOCO_ENCRYPTION_KEY`. If you ever change that key, all stored tokens become undecryptable and you'll have to re-enter every one. Pick it once at install and keep it backed up. + +## The default-branch gotcha { #the-default-branch-gotcha } + +The **Create Project** dialog defaults the branch to `main`, but several places in the backend assume `master`. **Set this field explicitly to match your repository's real default branch** (`main` or `master`) rather than trusting the pre-filled value. Getting it wrong is the most common first-run snag — branches get cut from, and PRs target, the wrong base. + +## Gate commands { #gate-commands } + +A developer agent runs a quality gate against its own work *before* it submits for QA. By default that's a sensible lint + typecheck pair, but you'll get far better results by pointing RoboCo at your repository's *real* checks: + +- **`quality_command`** — the fast pre-submit gate, run at the moment an agent says it's done (for example `make gate`). If you set this, it replaces the default lint/typecheck pair. Keep it fast; it runs on every submission. +- **`test_command`, `lint_command`, `format_command`, `typecheck_command`, `build_command`** — the individual commands for the cell's QA and CI steps. + +Setting these so they mirror what *you* would run locally is the single biggest lever on output quality: the company gates itself exactly the way you would. + +## Sandboxing agents away from a repo + +If you want to make sure agents can never point a project at a particular repository — RoboCo's own source, say, during a test run — set `ROBOCO_PROTECTED_GIT_URLS` to a comma-separated list of URL substrings. Creating or updating a project whose Git URL matches one is rejected. + +## What happens under the hood + +You don't have to manage any of this, but it's worth knowing what registering a project sets in motion: + +- The first time an agent is assigned work on the project, RoboCo clones the repo into that agent's own workspace under `ROBOCO_WORKSPACES_ROOT` (default `/data/workspaces`). **Every agent gets its own clone**, so they work in parallel without stepping on each other. That directory is the disk to provision and back up. +- Right after cloning, RoboCo **scrubs the token out of the clone's git config** and verifies no token byte survived anywhere under `.git/` — destroying the workspace if one did. Your PAT never lives inside an agent container. + +## Next + +→ **[Your first task](first-task.md)** — hand the company something to build. diff --git a/docs/get-started/first-task.md b/docs/get-started/first-task.md new file mode 100644 index 00000000..0a44c6c4 --- /dev/null +++ b/docs/get-started/first-task.md @@ -0,0 +1,46 @@ +# Your first task + +With a project registered, you can hand the company work. The way in is the **Task Assistant** — the Intake agent — on the **Prompter** page. You give it a rough idea; it reads your *actual* codebase, asks a few sharp questions, and hands back a properly-formed task with an objective, a per-cell breakdown, and the acceptance criteria that define "done." + +## Draft a task with the assistant + +1. Open the **Prompter** page (the **Task Assistant**). +2. Point it at the **project** you just registered (or a **product**, if you've mapped several repos together), and describe what you want — a feature, a fix, anything. +3. It spins up an agent that **clones the scope and reads the real code** before it says a word, then comes back with a grounded proposal: what to build, where it should live, and the acceptance criteria — citing your actual files and pages. +4. Refine it over a turn or two until the spec is right. + +When the proposal is ready, you choose on a single card: + +- **Keep chatting** — keep refining the draft. +- **Board review & Start** — send it to the Product Owner and Head of Marketing to sharpen the requirements before any code is written. +- **Approve & Start** — hand it straight to the Main PM. + +!!! tip "You don't have to use the assistant" + The assistant is the easiest path, but a task is just a record. You can also create one directly through the API (`POST /api/tasks`) if you're scripting RoboCo — every task needs a title, a description, at least one acceptance criterion, a team, and a target project. The API reference covers the full schema. + +## What happens after you approve + +The moment you approve, the company takes over: + +1. The **Main PM** breaks the task into per-cell subtasks and sets the cells running. +2. Each **cell PM** delegates to its developers, clears blockers, and triages. +3. **Developers** build it in their own clones and open pull requests; **QA** reads the real diff and passes or fails it; **Documenters** write down what was built. +4. A **PR reviewer** checks each assembled pull request before a PM merges it up the chain. +5. The **Main PM** opens the final pull request into `master` and notifies you it's done. + +You can watch all of this live — on the **Kanban** board, the **Tasks** table, the **Communications** stream, and the **Command Center**. Nothing happens in the dark. + +## The two moments you're needed + +A whole feature only needs you at **two points**: + +- **The green light at the start** — *Approve & Start* (or send it back). +- **The last call at the end** — the finished pull request lands in your **CEO Approval Queue**, where you **Approve & Merge**, **Request Changes**, or **Cancel**. Only you ever merge to `master`. + +Everything between those two moments is the company doing its job. + +## See it for real + +The best way to understand the whole journey is to watch one happen. **[The Tour](../how-to/README.md)** follows RoboCo building one of its *own* features — the Prompter you just used — from this same starting point all the way to a merged pull request, with screenshots at every step. + +To understand the machinery behind it — the roles, the gated lifecycle, the merge chain — read **[The Company](../company/index.md)**. diff --git a/docs/get-started/index.md b/docs/get-started/index.md new file mode 100644 index 00000000..fbc6a0ac --- /dev/null +++ b/docs/get-started/index.md @@ -0,0 +1,27 @@ +# Get Started + +Three steps take you from a cloned repository to a company working on your code: + +1. **[Install & first run](installation.md)** — bring up the whole stack with Docker, set two secrets, and open the Command Center. +2. **[Register your first project](first-project.md)** — point RoboCo at a git repository it's allowed to work on. +3. **[Your first task](first-task.md)** — hand the company something to build and watch it go end to end. + +You don't need Python, Node, or a database on your machine — everything runs in Docker. The one thing you provide is a way for the agents to reach a model: by default that's the **Claude Code** session already on your host, so there's no API key to wire up. + +!!! tip "Prefer to watch first?" + The [26-minute intro](https://www.youtube.com/watch?v=t1QNqJgBmkM) and the [full panel walkthrough](../how-to/README.md) are the fastest way to get the shape of the thing before you install it. + +## What you'll need + +| You need | Why | +|----------|-----| +| **Docker** + **Docker Compose** | The entire stack (PostgreSQL, Redis, Ollama, the orchestrator, the panel, nginx) runs as containers. | +| **A Claude Code auth directory** (`~/.claude`) | Mounted into the orchestrator so agents can reach the model. Run `claude` once on the host to create it. *(Or run the workforce on [Grok](installation.md#optional-run-on-grok-instead) instead.)* | +| **A GitHub Personal Access Token** | One per project you register, so agents can clone it and open pull requests. You add this in the panel later, not now. | +| **~10 GB of disk** and a few GB of RAM | The image set and the per-agent git clones. RoboCo is light at runtime — see the [resource notes](installation.md#resources). | + +## The shape of the system + +Everything is served behind a single address — **`http://localhost:3000`** — by an nginx reverse proxy. The browser only ever sees one origin; nginx routes `/api` and `/ws` to the orchestrator (FastAPI) and everything else to the Next.js control panel. That panel is your one window into the company. + +When you're set up and looking at the **Command Center**, head into [The Company](../company/index.md) to understand the org and the lifecycle, or take [the Tour](../how-to/README.md) to watch a real feature get built. diff --git a/docs/get-started/installation.md b/docs/get-started/installation.md new file mode 100644 index 00000000..ab841167 --- /dev/null +++ b/docs/get-started/installation.md @@ -0,0 +1,160 @@ +# Install & first run + +This page takes you from nothing to the **Command Center** open in your browser. The whole stack runs in Docker, so the only things you install on the host are Docker itself and (for the default Claude backend) the Claude Code CLI. + +## 1. Authenticate the model backend + +By default every agent runs on Anthropic Claude, using the **Claude Code** session on your host rather than a metered API key. Authenticate once: + +```bash +npm install -g @anthropic-ai/claude-code +claude # opens a browser to log in; creates ~/.claude +``` + +That `~/.claude` directory is mounted read-only into the orchestrator, which hands it to each agent container. If you'd rather run the workforce on Grok, skip this and see [Optional: run on Grok](#optional-run-on-grok-instead) below. + +## 2. Get the code + +```bash +git clone https://github.com/rennf93/roboco.git +cd roboco +cp .env.example .env +``` + +## 3. Set the two required secrets + +Open `.env` and set these two — the orchestrator refuses to start without the first, and Docker deployments need the second: + +```bash +# Encrypts every per-project GitHub token at rest (Fernet). REQUIRED. +ROBOCO_ENCRYPTION_KEY= + +# Signs the per-agent auth tokens (HMAC). REQUIRED for docker compose. +ROBOCO_AGENT_AUTH_SECRET= +``` + +Generate each one: + +```bash +# ROBOCO_ENCRYPTION_KEY +python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())' + +# ROBOCO_AGENT_AUTH_SECRET +python -c 'import secrets; print(secrets.token_hex(32))' +``` + +!!! danger "Keep `ROBOCO_ENCRYPTION_KEY` safe and stable" + This key encrypts the GitHub tokens you'll store per project. If you lose it or change it later, **every stored token becomes undecryptable** and must be re-entered. Back it up with your other secrets, and never commit `.env`. + +Everything else in `.env.example` has a working default for a local Docker run. On a NAS or remote server you'll also set the host-path variables (`ROBOCO_HOST_PROJECT_DIR`, `ROBOCO_HOST_CLAUDE_DIR`, `ROBOCO_DATA_DIR`) — those are covered in the deployment reference. + +## 4. Bring up the stack + +You have two ways to get the images. Both start from the clone above. + +=== "Pre-built images (quickest)" + + Every release publishes all RoboCo images to both the GitHub Container Registry and Docker Hub, so you can run the full stack without building anything: + + ```bash + docker compose -f docker-compose.registry.yml pull + docker compose -f docker-compose.registry.yml up -d + ``` + + Pick the registry and version with two variables (defaults shown): + + ```bash + ROBOCO_REGISTRY=ghcr.io/rennf93 # or docker.io/renzof93 + ROBOCO_VERSION=latest # or a pinned release, e.g. 0.8.0 + ``` + + The orchestrator pulls and spawns the matching pre-built agent images on demand — no build toolchain on your host. + +=== "Build from source" + + The same stack, built locally from the Dockerfiles instead of pulled: + + ```bash + docker compose up -d # builds images on first run, then starts everything + ``` + +The first start does real work: it pulls the local models into Ollama (the embedding model is quick; the local LLM is a couple of minutes), then the orchestrator waits for them, **runs the database migrations itself**, and indexes its knowledge base. You don't run migrations by hand — the stack brings its own schema up to date on every boot. + +## 5. Watch it come up + +```bash +docker compose logs -f orchestrator +``` + +Wait until the orchestrator reports it's serving, then open: + +```text +http://localhost:3000 +``` + +That's the **Command Center** — per-cell health, the CEO approval queue, live metrics, and recent activity. The company is up; it's just idle because you haven't given it a repository or a task yet. + +!!! note "One address for everything" + nginx is the only externally-exposed service. It serves the panel and proxies `/api` and `/ws` to the orchestrator, so the browser sees a single origin at `:3000`. You generally won't hit the orchestrator's own port directly. + +## Next + +→ **[Register your first project](first-project.md)** — point RoboCo at a repository it's allowed to work on. + +--- + +## Optional: run on Grok instead + +RoboCo can run the entire workforce on **xAI Grok** using xAI's official `grok` CLI on a **SuperGrok subscription** — no metered API key, so a run can't stall mid-task on exhausted credits. + +```bash +grok login # once, on the host — creates ~/.grok/auth.json +``` + +Then in `.env`: + +```bash +ROBOCO_HOST_GROK_DIR=/home/youruser/.grok # the real host ~/.grok to mount in +# ROBOCO_GROK_AGENT_IMAGE=roboco-agent-grok:latest +# ROBOCO_GROK_CLI_MODEL=grok-build +# ROBOCO_GROK_REASONING_EFFORT= # low|medium|high|xhigh|max (empty = model default) +``` + +The orchestrator keeps the short-lived Grok token refreshed for you, so agents don't hang on an expired login. Which agents run on which backend is set on the **Settings → AI Providers** page — that, and the per-role model assignments, are covered in the models section. + +## Optional: secure mode + +On a trusted LAN, RoboCo runs in **header-trust mode** by default: requests identify the caller by role headers, with no token required. That's fine on a private network and is the intended setup. + +If you need to harden it so an agent can't spoof another's role, set `ROBOCO_AGENT_AUTH_REQUIRED=true`, keep your `ROBOCO_AGENT_AUTH_SECRET`, and generate the panel's CEO token with `make panel-token` into `ROBOCO_PANEL_AGENT_TOKEN`. nginx injects that token so the panel keeps working without the browser holding the signing secret. The full security model is in the Configure & Deploy reference. + +!!! warning + Don't expose RoboCo to the public internet as-is. It's built to run on a trusted private network (homelab / LAN). + +## Resources { #resources } + +RoboCo is light at runtime. Agent containers are spawned on demand and torn down when their work is done, so you rarely have more than a handful live at once. + +| At idle (full stack, no task running) | RAM | +|---------------------------------------|-----| +| Ollama (models loaded) | ~2.2 GB | +| Orchestrator | ~150 MB | +| Postgres / Panel / Redis / nginx | ~120 MB combined | + +The whole standing stack idles around **~2.5 GB**, almost all of it Ollama. Under load — five agents working concurrently — the stack peaked around **~6.6 GB**; even at full-fleet peak you stay well under ~10 GB. **Storage is the larger footprint:** the agent images all build from a shared base layer, so on disk they cost far less than their nominal sizes summed. `docker system prune` reclaims old image versions, stopped containers, and build cache. + +```bash +docker stats # live RAM / CPU per running container +docker system df # image / container / build-cache disk usage +``` + +## Running on the host (for hacking on RoboCo itself) + +If you want to develop RoboCo's own code rather than just run it, you can run only the backing services in Docker and the app on your host. RoboCo's code needs **Python 3.13+** (`uv` fetches it if needed): + +```bash +uv sync +docker compose up -d postgres redis ollama # backing services only +uv run alembic upgrade head # migrate the database +uv run python -m roboco.cli # API + orchestrator +``` diff --git a/docs/how-to/01-the-company.md b/docs/how-to/01-the-company.md index 3026789a..5991ea7b 100644 --- a/docs/how-to/01-the-company.md +++ b/docs/how-to/01-the-company.md @@ -4,7 +4,7 @@ What keeps twenty-two agents from dissolving into noise is that RoboCo is relent Work in RoboCo is always a **task**, and tasks nest into a tree that mirrors the org itself: -``` +```text CEO (you, the human) ├── Intake (on-demand interviewer — drafts a task with you) ├── Secretary (your chief-of-staff — acts only on your command) diff --git a/docs/how-to/05-the-business-workflow.md b/docs/how-to/05-the-business-workflow.md index d058f881..fe404aee 100644 --- a/docs/how-to/05-the-business-workflow.md +++ b/docs/how-to/05-the-business-workflow.md @@ -52,4 +52,4 @@ The cleanest way to understand this layer is to walk it once, end to end: Previous: **[← The last call — and the loop](04-the-last-call-and-the-loop.md)** · Back to **[the index](README.md)** -*RoboCo is early-stage, work-in-progress software (v0) — expect rough edges. The [README](../../README.md) covers setup, architecture, and the security model.* +*RoboCo is early-stage, work-in-progress software (v0) — expect rough edges. The [Get Started guide](../get-started/index.md) covers setup, configuration, and the security model.* diff --git a/docs/how-to/README.md b/docs/how-to/README.md index ca0d4dcb..c0b04826 100644 --- a/docs/how-to/README.md +++ b/docs/how-to/README.md @@ -28,4 +28,4 @@ This guide follows one task — the Prompter's own build — from a rough idea t --- -*RoboCo is early-stage, work-in-progress software (v0) — expect rough edges. The [README](../../README.md) covers setup, architecture, and the security model.* +*RoboCo is early-stage, work-in-progress software (v0) — expect rough edges. The [Get Started guide](../get-started/index.md) covers setup, configuration, and the security model.* diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..ed26f7a7 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,77 @@ +--- +title: RoboCo +hide: + - navigation +--- + +# RoboCo + +**RoboCo is a self-hosted AI software company.** Twenty-five AI agents and one human — you — organized as a real engineering org: roles, a chain of command, formal reviews, and sign-offs. You don't wire up a swarm of bots or babysit a prompt loop. You run a company. You drop work in at the top, and the organization carries it all the way through planning, building, QA, review, and documentation, then brings it back to your desk for the final word. + +You act at the two ends. The company fills in everything between. + +![The RoboCo control panel: the org tree, a task in progress, and the CEO approval queue.](videos/panel-teaser.gif){ loading=lazy } + +!!! warning "Early-stage software (v0)" + RoboCo is a working prototype under active development. It runs in a homelab, has rough edges, and its API and database schema are not stable yet. It is **not production-ready** — don't expose it to the public internet as-is. Issues and pull requests are very welcome. + +<div class="grid cards" markdown> + +- :material-rocket-launch:{ .lg .middle } **Get Started** + + --- + + Go from a cloned repo to a running company looking back at you from the Command Center. + + [:octicons-arrow-right-24: Install & first run](get-started/index.md) + +- :material-sitemap:{ .lg .middle } **Understand the company** + + --- + + The org, the roles, the strict task lifecycle, and how every agent is structurally sandboxed. + + [:octicons-arrow-right-24: The Company](company/index.md) + +- :material-movie-open-play:{ .lg .middle } **Take the tour** + + --- + + Follow one feature — RoboCo building its own Prompter — from a rough idea to a merged pull request. + + [:octicons-arrow-right-24: The Tour](how-to/README.md) + +- :material-github:{ .lg .middle } **Read the source** + + --- + + RoboCo is open source under AGPL-3.0. The whole stack, agents included, is on GitHub. + + [:octicons-arrow-right-24: rennf93/roboco](https://github.com/rennf93/roboco) + +</div> + +## What you're running + +A single `docker compose up` brings up the whole stack behind one address — `http://localhost:3000` — and that one window is the company. Underneath it: + +- **A virtual organization.** A Board sets direction, a Main PM coordinates three delivery cells (Backend, Frontend, UX/UI), and an Auditor watches everything. You sit above all of it as CEO. [See the org →](company/org-and-roles.md) +- **A strict lifecycle.** Every piece of work is a task, no task moves without acceptance criteria, and each task walks the same gated path — built, QA'd, documented, reviewed, approved. The structure is what turns a roster of models into a company that ships. [See the lifecycle →](company/task-lifecycle.md) +- **Real git, real pull requests.** Each agent works in its own clone; work flows up a cell → root → master merge chain as actual pull requests on your repository. **Only you ever merge to `master`.** [See the merge model →](company/merge-model.md) +- **A hard trust boundary.** Agents never touch your API or your shell directly. They act only through a narrow set of role-scoped verbs, so a developer can't merge, QA can't commit, and the Auditor can't even speak. [See how agents are sandboxed →](company/agent-gateway.md) + +## Watch it first + +If you'd rather see it than read about it: + +- [**The 26-minute intro**](https://www.youtube.com/watch?v=t1QNqJgBmkM) — what RoboCo is, a walkthrough, and how to use it. +- [**The 2.5-hour build session**](https://www.youtube.com/watch?v=xige_EUIjIA) — a single conversation taken all the way to a shipped feature. +- [**The 2:33 panel walkthrough**](videos/panel-full-walkthrough.mp4) — every page of the control panel, end to end. + +## How models are powered + +Agents run on **Anthropic Claude** by default, authenticated from the Claude Code session you already have on the host — no metered API key to wire up. You can also run the whole workforce on **xAI Grok** (the official `grok` CLI on a SuperGrok subscription), or point roles at **local / self-hosted** models. [Choosing & running models →](get-started/index.md) + +--- + +RoboCo is licensed under the [GNU Affero General Public License v3.0](https://github.com/rennf93/roboco/blob/master/LICENSE). Contributions are welcome and require a signed [Contributor License Agreement](https://github.com/rennf93/roboco/blob/master/CLA.md), automated on your first pull request. diff --git a/docs/models/grok.md b/docs/models/grok.md new file mode 100644 index 00000000..8559396b --- /dev/null +++ b/docs/models/grok.md @@ -0,0 +1,56 @@ +# Run on xAI Grok + +RoboCo can run the entire workforce — or just some agents — on **xAI Grok**, using xAI's official `grok` CLI on a **SuperGrok subscription** rather than a metered API key. That matters operationally: a subscription can't run out of credits mid-task, so a Grok fleet won't stall halfway through a delivery the way a metered key can. Grok reaches full parity with the Claude path by construction: the same MCP gateway, the same per-role tool manifest, the same prompt-injection and bash guards, the same per-agent cost capture. + +## Set it up once + +Authenticate Grok on the host with the official CLI, then point RoboCo at the resulting directory: + +```bash +grok login # once, on the host — creates ~/.grok/auth.json +``` + +Then in `.env`: + +```bash +ROBOCO_HOST_GROK_DIR=/home/youruser/.grok # the REAL host ~/.grok to mount in +``` + +The orchestrator mounts that directory's `auth.json` **read-only** into each Grok agent container. On a NAS or any docker-in-docker deploy the orchestrator's home is not the host's home, so `ROBOCO_HOST_GROK_DIR` must point at the actual host path where `grok login` wrote `~/.grok` — otherwise agents start with no credential. + +Finally, on **Settings → AI Providers** set the routing mode to **Grok** (whole fleet) or pin individual agents to a Grok model in **Mix** mode. See [Choosing a provider](./provider-routing.md) for the modes and precedence. The [installation guide](../get-started/installation.md#optional-run-on-grok-instead) covers the same first-run steps inline. + +## The token refreshes itself + +The Grok access token has a fixed ~6-hour, server-set lifetime, and the CLI can't refresh it headlessly — on an expired token it would hang forever at an interactive login prompt. RoboCo handles this for you: + +- Once per dispatch tick the orchestrator mints a fresh token from the offline-access refresh token (xAI's OIDC `refresh_token` grant) before expiry and atomically rewrites the shared `auth.json` in place. The refresh fires `ROBOCO_GROK_AUTH_REFRESH_SKEW` seconds (default `1800`) ahead of expiry. +- As a backstop, each agent's entrypoint runs `python -m roboco.llm.providers.grok_auth --check` and **refuses to start** on a missing or expired token instead of hanging. + +!!! warning "The orchestrator's `~/.grok` mount must be writable" + The orchestrator rewrites `auth.json` when it refreshes the token, so the orchestrator's own mount of `~/.grok` must be **read-write**. (The per-agent mount stays read-only — agents only read the credential.) If the orchestrator can't write it, the token will expire and Grok agents will fail their start-up `--check`. + +## Per-fleet tuning + +Three optional knobs let you trade cost against quality across the whole Grok fleet. All are env-set and apply uniformly (no per-role reduction — that's parity with the Claude path): + +| Variable | Default | What it does | +|----------|---------|--------------| +| `ROBOCO_GROK_REASONING_EFFORT` | unset (model default) | The `grok --effort` level. Set `low` / `medium` / `high` / `xhigh` / `max` to dial cost vs quality; empty, `default`, or `full` keeps the model's own default. | +| `ROBOCO_GROK_MAX_TURNS` | `200` | The per-agent turn cap for a `grok -p` run. | +| `ROBOCO_GROK_MAX_COST_USD` | `0` (off) | A per-agent cost ceiling in USD. The grok CLI has no live usage hook, so the orchestrator reads each running container's captured cost and kills it once it crosses this ceiling — a backstop against a runaway loop. `0` disables the cap. | + +Two more variables exist mostly for staged rollouts and tests: + +| Variable | Default | What it does | +|----------|---------|--------------| +| `ROBOCO_GROK_AGENT_IMAGE` | `roboco-agent-grok:latest` | Override the Grok agent Docker image. | +| `ROBOCO_GROK_CLI_MODEL` | `grok-build` | The grok CLI model id passed to agents. | + +!!! note "Usage and cost still show up" + Each Grok agent gets a per-agent usage directory mounted read-write so its token/cost is read back when the session finalizes. Grok traffic therefore lands on the same [usage dashboard](../operations/cost-and-usage.md) as Claude, priced from the captured session totals. + +## Next + +- [Choosing a provider](./provider-routing.md) — set the Grok mode or pin agents in Mix. +- [What keeps a run alive](./resilience.md) — Grok agents park-and-resume on a rate limit like any other provider. diff --git a/docs/models/index.md b/docs/models/index.md new file mode 100644 index 00000000..2cfef661 --- /dev/null +++ b/docs/models/index.md @@ -0,0 +1,25 @@ +# Choosing & Running Models + +RoboCo's agents are backend-agnostic. By default the whole workforce runs on **Anthropic Claude**, authenticated from the Claude Code session on your host — no API key to manage. You can route the company onto **xAI Grok** instead, or point individual agents and roles at **local / self-hosted** models, all from one page in the panel. + +<div class="grid cards" markdown> + +- **[Provider routing](provider-routing.md)** + + --- + + The Settings → AI Providers page: the routing modes, agent-over-role-over-global precedence, saved keys, and the fail-soft fallback. + +- **[Running on Grok](grok.md)** + + --- + + The whole workforce on xAI Grok via the official CLI and a SuperGrok subscription — no metered key. + +- **[Resilience](resilience.md)** + + --- + + What keeps a run alive: crash auto-retry, and how rate limits and provider overloads park work instead of dropping it. + +</div> diff --git a/docs/models/provider-routing.md b/docs/models/provider-routing.md new file mode 100644 index 00000000..486c1ace --- /dev/null +++ b/docs/models/provider-routing.md @@ -0,0 +1,60 @@ +# Choosing a provider + +By default every agent runs on Anthropic Claude — no key, no config. The orchestrator mounts your host `~/.claude` session into each container and that's the whole setup. When you want something else — the whole fleet on Grok, a few agents on a self-hosted model, or a deliberate mix — you don't edit env files. You set it on **Settings → AI Providers** (`/settings/ai-providers`, linked from the sidebar footer), and the choice is stored server-side and resolved per agent at spawn. + +## The routing modes + +The page has one global **routing mode** that decides which backend every agent uses unless you override it per agent: + +| Mode | What it does | Needs | +|------|--------------|-------| +| **Anthropic** | Every agent runs on Claude via the mounted `~/.claude` auth. The default. | Nothing — the orchestrator's `~/.claude` mount | +| **Grok** | Every agent runs on xAI Grok (`grok-build`). | A saved Grok key *or* a SuperGrok login — see [Run on Grok](./grok.md) | +| **Ollama** | Every agent runs on Ollama Cloud. | A saved Ollama Cloud key | +| **Self-Hosted** | Every agent runs against your own OpenAI-compatible endpoint. | A base URL with a successful **Test Connection** | +| **Mix** | Pin individual agents to specific models; everything else inherits the global default. | Whatever the pinned models need | + +Anthropic, Ollama Cloud, and self-hosted models all speak the Anthropic Messages wire protocol, so they run *through* the same Claude Code runtime — the orchestrator just injects the endpoint and key. Only Grok speaks a different protocol and runs in its own agent image. You don't have to think about that distinction; it's just why Grok is the one mode with a separate setup page. + +!!! note "The CEO seat is not an agent" + Mix mode shows a per-agent table mirroring the org chart, but you (the CEO) are intentionally excluded — there's no model to assign to the human seat. The Intake (Prompter) and Secretary chats *are* in the table. + +## How a route is resolved: agent > role > global + +When the orchestrator spawns an agent, it picks the model by a three-rung precedence ladder, most specific first: + +1. **Agent** — a model pinned to that exact agent (set in Mix mode). +2. **Role** — a model assigned to the agent's role. +3. **Global** — the global mode's default. + +The first rung that has an assignment wins. So in Mix mode you can pin `be-dev-1` to one model, leave the rest of the backend cell on the global default, and the dispatcher resolves each one independently at spawn time. Switching the global mode **clears all per-agent overrides** (the panel asks you to confirm), so flip to Mix *after* you've chosen your baseline, not before. + +## Saving keys and endpoints + +Three secrets live on this page, all stored **Fernet-encrypted** server-side and **never returned by the API** — the panel only ever sees a `has_key` / `has_token` boolean, so you re-enter (you can't read back) a key: + +- **Grok (xAI) key** — only needed if you authenticate Grok with a key rather than a SuperGrok login. +- **Ollama Cloud key** — required before any agent can route to Ollama Cloud. +- **Self-hosted endpoint** — a base URL plus an optional auth token. Hit **Test Connection** to confirm reachability and auto-discover the model list; **Refresh** re-discovers it. A self-hosted assignment won't save until the connection tests clean. + +The panel guards you client-side: it blocks saving a Grok / Ollama / self-hosted assignment when the matching key or connection is missing, and warns when an Ollama-routed agent has no key (see below). + +## Fail-soft: misconfig falls back to Anthropic + +Routing is deliberately fail-soft. A *stalled* spawn is worse than a *wrong* model, so on any of these the orchestrator silently degrades that agent to Anthropic Claude rather than refusing to start it: + +- no assignment found for the agent, its role, or the global default; +- a self-hosted or Ollama endpoint that's unreachable (the orchestrator probes `/api/tags` before committing to it); +- a stored key or token that fails to decrypt. + +!!! warning "A quiet fallback can look like success" + Because the fallback is silent, an agent you *think* is on your self-hosted model may actually be running on Claude — for example if the endpoint went unreachable after you tested it. The fallback is logged in the orchestrator (`docker compose logs orchestrator` shows the warning). If you're routing away from Anthropic, watch the per-model breakdown on the [usage dashboard](../operations/cost-and-usage.md) to confirm the traffic is landing where you intended. + +!!! info "Providers are seeded by the migrations" + The Anthropic, Grok, Ollama Cloud, and self-hosted provider rows are created by `alembic upgrade head`, which the stack runs for you on boot. The AI Providers page only sets keys, mode, and assignments — it never creates providers. If you skipped migrations you'll see "provider not seeded" errors here. + +## Next + +- [Run on Grok](./grok.md) — the SuperGrok path end to end. +- [What keeps a run alive](./resilience.md) — crash retry and provider park-and-resume. +- The same guardrails apply on every backend — see [how agents are sandboxed](../company/agent-gateway.md). diff --git a/docs/models/resilience.md b/docs/models/resilience.md new file mode 100644 index 00000000..cf124a09 --- /dev/null +++ b/docs/models/resilience.md @@ -0,0 +1,43 @@ +# What keeps a run alive + +A long delivery run spans hundreds of spawned containers, and the things that can interrupt one — a model crash, a provider rate limit, a transient overload — are normal, not exceptional. RoboCo is built so none of those quietly kills the work or burns your tokens crash-looping. Two mechanisms do the heavy lifting: **crash auto-retry then escalate**, and **provider park-and-probe**. Both run server-side in the orchestrator, the same way on every backend. + +## Crash: auto-retry, then escalate once + +When an agent container stops, the orchestrator inspects how it exited: + +- **A clean exit** (the agent called `i_am_idle` — it ran out of work) resets that agent's error count. Nothing to do. +- **A provider overload** (see below) parks the provider instead of treating it as a crash. +- **A genuine crash** goes to retry-or-escalate. + +On a real crash the orchestrator bumps the agent's error count and **respawns it on the same task** — up to a cap of **3** retries. At exactly the cap it escalates **once** to a human notification (the agent is stranded), then stays quiet so a hard failure can't spam you. The respawn is cold (a fresh container), but it picks the task back up, so a one-off crash is invisible in practice. + +```mermaid +flowchart LR + A[Container stopped] --> B{How did it exit?} + B -->|i_am_idle, clean| C[Reset error count] + B -->|529 / 500 / 503 overload| D[Park the provider] + B -->|crash| E{error_count < 3?} + E -->|yes| F[Respawn on same task] + E -->|no, == 3| G[Escalate once to a human] +``` + +## Rate limits & overloads: park, probe, resume + +When a provider pushes back, retrying immediately just burns tokens against a wall. So instead of crash-looping, RoboCo **parks** that provider and queues its work: + +- A **rate limit (HTTP 429)** parks the provider. The agent reports `i_am_blocked(reason="rate_limited")`, the spawn gate stops launching new work for that provider, and a background loop probes for recovery. +- A **persistent overload (HTTP 529 / 500 / 503)** parks the same way. The model SDK already retries genuinely transient blips; a *persistent* overload is detected from the dead container's log markers and parked rather than crash-retried straight back into the overload. This is gated by `ROBOCO_OVERLOAD_BREAK_ENABLED`, which is **on by default**. + +The crucial property: **work is queued, never dropped.** Parked tasks wait; the background probe-and-resume loop requires a real `2xx` from the provider before it lifts the park and revives the parked agents. When the provider recovers, the queued work flows again on its own — you don't restart anything. + +!!! info "The amber banner" + While a provider is parked you'll see an amber banner across the panel: a per-provider countdown, how many agents are affected, and "operations paused — resuming automatically." It clears itself when the provider recovers. An empty banner means nothing is parked. The banner is driven live over the `/ws/system` WebSocket and re-syncs over HTTP if the socket drops — so a quiet provider reads as *paused, resuming*, not as a hang. + +!!! tip "Parked is not stuck" + If a run goes quiet, check the banner before assuming something broke. A parked provider with a counting-down timer is RoboCo waiting out a rate limit on purpose. The work is held and will resume — there's nothing for you to do. + +## Next + +- These guardrails are part of the broader [agent gateway](../company/agent-gateway.md) — agents are structurally constrained, not trusted to behave. +- [Choosing a provider](./provider-routing.md) and [running on Grok](./grok.md). diff --git a/docs/operations/cost-and-usage.md b/docs/operations/cost-and-usage.md new file mode 100644 index 00000000..520d8bc1 --- /dev/null +++ b/docs/operations/cost-and-usage.md @@ -0,0 +1,97 @@ +# Cost & usage + +RoboCo measures what your workforce spends in tokens and dollars, and shows it to you live in the panel. This page explains where those numbers come from, how to read the dashboard, how each provider is priced, and what cost controls exist (and where they don't). + +## How spend is measured + +Spend is captured **per agent session** — one container, doing one stretch of work. Each spawned agent runs a small in-container SDK server that watches the agent's own transcript and exposes its running token counts (input, output, cache-read, cache-write). The orchestrator runs a background sweep every **~60 seconds**: for every active agent it pulls the live counts, writes a usage snapshot, and updates the open session row so the database reflects progress mid-run. When the container stops, the session is **finalized** — final token counts are resolved, priced through the built-in cost table, and written to the closed session row along with the exit reason. + +A separate daily rollup keeps a rolling per-day, per-agent, per-team, per-model tally that feeds the "today" figures. + +!!! note "Why mid-run numbers move, and why a crash can read $0" + Because cost is computed per session and firms up only when the session ends, the figures for a still-running agent update roughly every minute and aren't final. A session that crashes or is abandoned before its transcript is read can finalize at a low or zero value. The dashboard is an accurate ledger of *closed* sessions plus a live estimate for the ones still open — not a real-time invoice. + +Grok agents have no live SDK hook; the orchestrator reads each Grok container's `usage.json` instead, and uses the same file to enforce the per-agent Grok cost cap (below). + +## The Token Usage & Cost dashboard + +Two panel surfaces show spend: + +- The **command center** carries a **Token Usage & Cost** card that streams live while agents work. It listens on the `/ws/system` WebSocket and shows a **Live** badge when connected; if the socket drops it falls back to HTTP polling and shows **Polling** (or **Connecting**). See [the command center](../panel/command-center.md). +- The **Metrics** page has a dedicated **Token Usage** tab with the full breakdown. See [Metrics](../panel/metrics.md). + +A **24h / 7d / 30d** selector drives every panel. What you get: + +| Panel | What it shows | +|-------|----------------| +| Summary | Input, output, and total tokens for the period, plus total cost in USD | +| Trend | Percent change vs the immediately prior window of equal length | +| Time series | Hourly points for 24h; daily points for 7d/30d | +| Per-model | Donut of cost share by model | +| Per-agent / per-team | Bar charts of spend, each with its share of the total | +| Monthly projection | Forecast spend (see below) | +| Cache efficiency | Cache hit-rate and the dollars prompt-caching is saving | +| Recent sessions | Raw recent spawn-session rows (default 50) | + +!!! info "Totals count all four token classes" + The total-token and cost figures sum **input + output + cache-read + cache-write**. Prompt-caching reads are cheap but not free, so they show up in both the totals and the cache-savings panel. + +!!! warning "Projection and cache savings are estimates" + The monthly projection is a naive extrapolation: the average daily cost over the last 7 days × 30. It's a planning forecast, not a bill, and it overstates if you spun up the fleet for a one-off burst. The cache-savings figure uses a single aggregate baseline rate to estimate what the cached tokens *would* have cost at the full input price — treat it as indicative, not exact. + +## How each provider is priced + +Pricing is provider-aware, from a built-in USD-per-million-token table (`roboco/billing/pricing.py`). Model names are matched on substring, longest fragment wins. + +| Provider / model | Input | Output | Priced? | +|------------------|-------|--------|---------| +| Claude Opus 4 | $5.00 | $25.00 | yes (cache read $0.50, write $6.25) | +| Claude Sonnet (4 / 3.7 / 3.5) | $3.00 | $15.00 | yes (cache read $0.30) | +| Claude Haiku 4 / 3.5 | $1.00 | $5.00 | yes | +| xAI `grok-build` | $1.00 | $2.00 | yes (cache read $0.20) | +| Local Ollama (`ollama/…` or bare tag) | — | — | **$0 by design** | +| Ollama Cloud (`:cloud` tag) | — | — | **$0 by design** | + +!!! note "$0 for local and Ollama Cloud is not a bug" + Self-hosted Ollama runs on hardware you own and Ollama Cloud is billed by flat subscription, so neither carries a per-token cost. RoboCo intentionally prices them at $0 — it is not undercounting, and you won't see a warning. + +!!! warning "A brand-new Claude model can silently undercount" + If a `claude`-named model isn't in the pricing table, RoboCo logs a warning and returns $0 for it rather than crashing. That **is** real spend going uncounted. If you point an agent at a Claude model newer than this build and see $0 cost on a busy fleet, the table needs the new rate added — check the orchestrator logs for the pricing warning. + +## Cost controls (and the asymmetry) + +There is exactly one built-in dollar cap, and it only covers Grok: + +| Setting | Default | What it does | +|---------|---------|--------------| +| `ROBOCO_GROK_MAX_COST_USD` | `0.0` | Per-agent Grok cost ceiling in USD, read from the container's `usage.json`. The orchestrator kills a Grok container once it crosses this, catching runaway-loop token burn. `0` disables it. **Grok agents only.** | + +!!! danger "There is no built-in dollar cap for Claude agents" + The Claude path is **observe-only** on cost. RoboCo will show you Claude spend live and historically, but it will not auto-kill a Claude agent for crossing a dollar threshold — there is no `ROBOCO_*_MAX_COST_USD` equivalent for it. Your protection against runaway Claude spend is the structural one: rate-limit and overload **park-and-probe** (a provider 429 or persistent overload queues the agent and probes for recovery instead of retrying in a hot loop — see [provider resilience](../models/resilience.md)), the per-task verb gateway, and watching the dashboard. Budget accordingly. + +A related guard reaps abandoned interactive chats so they don't leak a container: + +| Setting | Default | What it does | +|---------|---------|--------------| +| `ROBOCO_INTERACTIVE_IDLE_REAP_SECONDS` | `1800` | Reaps an idle live Intake/Secretary chat (by time since last turn) so it stops holding a container and burning tokens. `0` disables. | + +## Quick checks from the shell + +```bash +# Period summary (totals + cost + trend) +curl -s 'http://localhost:3000/api/usage/summary?period=7d' + +# Monthly projection +curl -s http://localhost:3000/api/usage/projection + +# Container-level resource use (not token cost) +docker stats +docker system df +``` + +## Next + +- Read the live dashboard on [the command center](../panel/command-center.md) and the full breakdown on [Metrics](../panel/metrics.md). +- See where the live stream comes from in [WebSockets](../api/websockets.md) and the `/api/usage/*` routes in the [REST API](../api/rest-api.md). +- Understand the park-and-probe spend protection in [provider resilience](../models/resilience.md). +- For operational (non-cost) health, see [health & metrics](./health-and-metrics.md). diff --git a/docs/operations/health-and-metrics.md b/docs/operations/health-and-metrics.md new file mode 100644 index 00000000..9ebe0447 --- /dev/null +++ b/docs/operations/health-and-metrics.md @@ -0,0 +1,58 @@ +# Health & metrics + +Two different questions an operator asks: *is the system up?* and *is the workforce productive?* RoboCo answers the first with two HTTP probes, and the second with an operational Metrics view. Neither has anything to do with token cost — for spend, see [cost & usage](./cost-and-usage.md). + +## Liveness and readiness probes + +The orchestrator exposes two probes behind nginx (so they're reachable at `localhost:3000/api/...`, or directly on the orchestrator): + +| Endpoint | Checks | Returns | +|----------|--------|---------| +| `GET /api/health` | The process is up | Always `200` with `{ status: "ok", version, environment }` once the app is serving | +| `GET /api/ready` | Database (`SELECT 1`) **and** Redis (`PING`) | `200` with `{ status, database, redis }`; `status` is `ok` only when both pass, otherwise `degraded` | + +```bash +curl -s http://localhost:3000/api/health +curl -s http://localhost:3000/api/ready +``` + +!!! info "Readiness is the one to watch" + `/api/health` is a liveness probe — it stays `200` as long as the process answers, so it can't tell you a dependency is down. `/api/ready` is the real readiness signal: it actively pings Postgres and Redis and reports `degraded` (still HTTP `200`, but `status: "degraded"` with the failing dependency's error in the `database`/`redis` field) when either is unreachable. Point your uptime monitor at `/api/ready` and alert on `degraded`. + +!!! note "Startup ordering" + The app-level root `/health` used during container startup is additionally gated on the in-house RAG engine being operational, which is why the orchestrator can take a minute to report healthy after a cold start while it indexes documents. See [deployment](../deploy/deployment.md) for the full startup sequence. + +## The Metrics → Performance view + +The **Metrics** page has a **Performance** tab driven by tasks, messages, and notifications — your read on whether work is actually flowing. See the panel walkthrough in [Metrics](../panel/metrics.md). + +What it surfaces: + +| Group | Metrics | +|-------|---------| +| Velocity | Tasks completed vs created, average completion time (hours), completion rate | +| Blockers | Active blockers, average and longest blocked time, blockers by team | +| Team & agent | Per-team and per-agent performance | +| Communication | Message/notification volume | + +### The ok / slow / critical health signal + +Org and per-team health roll up to a single status driven mainly by the **blocked-task ratio**: + +```mermaid +flowchart LR + A[blocked / total tasks] -->|ratio > 0.3| C[critical] + A -->|ratio > 0.15| S[slow] + A -->|otherwise| K[ok] +``` + +A team with more than 30% of its tasks blocked reads **critical**; over 15% reads **slow**; below that, **ok**. A heuristic also flags a team sitting on stale active tasks with zero completions. When you see **slow** or **critical**, the Blockers panel tells you where the work is jammed and which team owns it — chase the longest-blocked tasks first. + +!!! tip "Health is about flow, not errors" + This status is computed from task state, not exceptions or crashes. A green org-health with a `degraded` `/api/ready` means the infrastructure is wobbling even though the backlog looks healthy — watch both signals, they answer different questions. + +## Next + +- Walk the panel surface in [Metrics](../panel/metrics.md) and [the command center](../panel/command-center.md). +- For token spend and the cost dashboard, see [cost & usage](./cost-and-usage.md). +- For the startup sequence and what each container needs, see [deployment](../deploy/deployment.md). diff --git a/docs/operations/index.md b/docs/operations/index.md new file mode 100644 index 00000000..e887736b --- /dev/null +++ b/docs/operations/index.md @@ -0,0 +1,19 @@ +# Cost & Observability + +RoboCo measures what the company spends and how it's performing, and shows both in the panel. This section explains the cost model and the operational signals. + +<div class="grid cards" markdown> + +- **[Cost & usage](cost-and-usage.md)** + + --- + + How token spend is measured, the Token Usage & Cost dashboard, provider-aware pricing (local is $0 by design), and the cost caps. + +- **[Health & metrics](health-and-metrics.md)** + + --- + + The health and readiness probes, and the delivery-performance metrics — velocity, blockers, and team health. + +</div> diff --git a/docs/optional/conventions.md b/docs/optional/conventions.md new file mode 100644 index 00000000..5b82f40c --- /dev/null +++ b/docs/optional/conventions.md @@ -0,0 +1,125 @@ +# Architectural Conventions Standard + +The `make`-style gates you configure per project (ruff, mypy, eslint, your test suite) answer one question: *is the code valid?* They cannot answer a second one a senior reviewer would catch on sight: *does this code live where it belongs?* The Architectural Conventions Standard is RoboCo's answer to that second question. It gives each project a repo-canonical architecture map and a tree-sitter validator that **hard-gates** an agent from landing a model defined inside a router, a route handler that runs its own SQL, a React component that fetches its own data, or a `# noqa` / `# type: ignore` that quietly silences the linter. + +It is **off by default** behind `ROBOCO_CONVENTIONS_ENABLED`. Every hook — the auto-scaffold, the prompt injection, the per-task constraints, and the gates — is fully inert until you turn it on. + +## What it does + +When enabled, the standard reaches the work in two directions. It *guides* agents up front — an "Architectural Standard" block is injected into every agent's prompt at spawn, and a `## Constraints` section listing the project's block-level rules and module boundaries is auto-attached to every task. And it *enforces* the same rules at the gate — a `block`-level finding refuses `i_am_done` (the developer's pre-submit) and `pr_pass` (the [in-path PR gate](pr-review.md)) with the offending `file:line` and a fix hint, while QA sees the findings in its review evidence. + +The rules live in a per-project `.roboco/conventions.yml` with four curated parts plus a set of structural checks: + +| Part | What it is | +|------|-----------| +| **Module map** | Path prefixes mapped to a human purpose and the definition *kinds* forbidden there (`model`, `route`, `helper`, `business_logic`, `component`). "`routers/` is for HTTP routes — no models, no helpers." | +| **Rules** | A toggleable rule set. Each rule fires at `warn` (advisory, never blocks) or `block` (refuses the gate). | +| **Custom rules** | Project-specific regex rules — a pattern, a message, and a level, optionally scoped to languages. | +| **Waivers** | Accountable per-`(path, rule)` escape hatches with a written reason — the sanctioned way to relieve a false positive, reviewed in the PR. | + +### Placement, hygiene, and modularity checks + +The validator runs four check families over each changed file: + +- **Placement** — a definition whose *kind* is forbidden in its module (a model in a router). +- **Hygiene** — the universal, stack-agnostic house-style rules seeded into every project: `no_lint_suppressions` (`block` by default — no `# noqa`, `# type: ignore`, `eslint-disable`) and `no_inline_comments` (`warn`). +- **Custom** — your project-specific regex rules. +- **Modularity** — separation-of-concerns judgements the linters are blind to, inspecting a file's *composition* and a definition's *body*, not just its top-level kind: + +| Rule | Fires when | Default level | +|------|-----------|---------------| +| `modular_cohesion` | One file mixes architectural concerns (e.g. a model *and* a route *and* a component) | `block` | +| `thin_routes` | A route handler does its own data access (SQLAlchemy `execute`/`commit`/`select`…) instead of delegating to a service | `block` | +| `thin_components` | A React component fetches data in its body instead of through a hook | `block` | +| `god_class` | A class grows past 15 methods (single-responsibility smell) | `warn` | + +!!! info "Precision over recall" + Every check fires only on a confident, structural signal, and abstains when it is uncertain — so a `block`-level gate is never tripped by a guess. If the validator genuinely *cannot* run on a diff (a parse or grammar error), it is **fail-loud**: it exits non-zero and the gate blocks rather than passing silently. + +## The effective map: defaults, present, absent, or partial + +Consumers never read the raw committed file — they read the **effective map**, so behaviour is identical whether `.roboco/conventions.yml` is present, absent, or partial. `ConventionsService` (`roboco/services/conventions.py`) builds it by auto-deriving a baseline from a repo scan (it infers modules from directory names like `routers/`, `models/`, `services/`, `components/`, `hooks/`; detects languages by file extension; seeds the universal hygiene rules) and then overlaying the committed file on top. The result is cached per `(project, HEAD sha)`. + +That is the load-bearing property: **the standard is enforced even before any file is committed.** A project with no `.roboco/conventions.yml` still gets sensible auto-derived rules and is gated by them. Resilience is built in — a missing file degrades to the auto-derived defaults, and an unparseable file falls back to the last-good cached map (status `degraded`) so the standard is never silently switched off by a typo. + +## The per-project Conventions editor + +Each project carries a **Conventions** tab in its edit dialog (panel component `panel/src/components/conventions/conventions-tab.tsx`). From there you manage the whole standard without hand-editing YAML: + +- **Module boundaries** — add/remove modules, set each one's path and purpose, and click a kind badge (`no model`, `no route`, `no helper`, `no business_logic`, `no component`) to toggle whether it is forbidden there. +- **Rules** — flip each rule between `warn` and `block` with a switch. +- **Custom rules** — add a regex `id`/pattern/message and set its level. +- **Waivers** — exempt a file from a rule with a written reason. +- **Recent violations** — the latest findings recorded across this project's tasks, each tagged with its rule and level. + +Two state banners keep you honest about what is in force: + +!!! note "Using auto-derived defaults" + When no file is committed yet, the tab shows a neutral **"Using auto-derived defaults"** banner — *not* an error. These rules are already enforced. The Save button reads **"Save defaults to repo"**: one click backfills the canonical file. (The earlier read-only, "missing"-as-broken behaviour is gone — this is the full editor with one-click backfill.) + +!!! warning "Conventions degraded" + If a committed file won't parse, an amber **"Conventions degraded"** banner appears. The effective map has fallen back to the last-good cache plus defaults; **Restore from last-good** re-commits the previous working file. + +### Save and Restore open a PR + +The standard is repo-canonical, so the editor never edits files behind your back. **Save to repo** and **Restore from last-good** each open a pull request against the project (via `GitService.open_conventions_pr`) carrying the new `.roboco/conventions.yml`. You review and merge it like any other change. If the project's workspace isn't cloned yet, the change is prepared on the branch and the toast tells you no remote PR was opened. + +## How agents are hard-gated + +```mermaid +flowchart TD + A[Developer finishes work] --> B[i_am_done] + B --> C{conventions validator<br/>on changed files} + C -->|block finding<br/>or could-not-run| D[REJECTED<br/>file:line + fix hint] + C -->|warn-only / clean| E[proceeds to QA] + D --> F[Dev fixes or commits a waiver] + F --> B + E --> G[QA sees findings in claim_review evidence] + G --> H[PR assembled] + H --> I[pr_pass at the in-path gate] + I --> J{same validator} + J -->|block / could-not-run| K[REJECTED back to needs_revision] + J -->|clean| L[PM merges] +``` + +Enforcement is deterministic and lives at two gateway choke points (`roboco/services/gateway/choreographer/`): + +- **`i_am_done`** — the developer's pre-submit. A `block`-level finding refuses the verb with the offending `file:line` and a fix hint; the findings are also persisted to the project's violations feed (even the ones that blocked). +- **`pr_pass`** — the [in-path PR-review gate](pr-review.md). The same validator runs on the assembled PR's changed files; a `block` finding fails it back to `needs_revision`. +- **QA evidence** — when QA calls `claim_review`, the convention findings ride along in its review briefing so the human-style review sees them too. + +A false positive is relieved one way only: the developer commits a **waiver** in their branch — an accountable `(path, rule, reason)` entry reviewed in the PR — never a silent in-code `# noqa`. + +## Enable it + +You can turn the standard on two ways; the effect is the same. + +=== "Panel" + + Open **Settings → Feature Flags** and toggle **"Enforce a per-project architectural standard"** on. + + !!! note "Takes effect on the next backend restart" + A feature-flag toggle persists in the settings store and is applied on the **next backend restart** — it is not a hot reload. Flip it, then restart the orchestrator. + +=== "Environment" + + Set the flag in your compose/env and restart: + + ```bash + ROBOCO_CONVENTIONS_ENABLED=true + ``` + +An unset flag falls back to its config default (off). See the [environment reference](../deploy/env-reference.md) for the full flag list. + +## What changes when it's on + +- Every agent's prompt carries the project's "Architectural Standard" block, and every project task gets an auto-attached `## Constraints` section. +- `i_am_done` and `pr_pass` block on `block`-level findings; QA sees findings in its review evidence. +- The project's **Conventions** tab is live — view the effective map and health, edit it, and Save/Restore via a PR. +- The validator CLI (`python -m roboco.conventions check --root <repo> --files …`) runs inside the agent image (the Python and TypeScript tree-sitter grammars are shipped there). + +When the flag is off, none of the above happens — no prompt block, no constraints, no gating, no scaffold. + +## Next + +→ [Toolchain matching](toolchain-matching.md) closes a different hollow-pass hole · [The in-path PR gate](pr-review.md) is where `pr_pass` runs · back to [Optional subsystems](index.md). diff --git a/docs/optional/index.md b/docs/optional/index.md new file mode 100644 index 00000000..53df8679 --- /dev/null +++ b/docs/optional/index.md @@ -0,0 +1,25 @@ +# Optional subsystems + +Beyond the core delivery loop, RoboCo ships a set of optional engines — most **off by default** — that you turn on when you want them. They're the "company-in-a-box" capabilities and the stricter quality gates. This page is the reference for all of them; each has its own page below. + +## The Feature Flags card + +You toggle these from **Settings → Feature Flags** in the panel rather than hand-editing environment variables. A toggle persists in the settings store and **takes effect on the next backend restart**. The matching `ROBOCO_*` environment variable is the same switch at the source — an unset flag falls back to its environment/config default, and some flags carry extra configuration (an API key, a project slug) that only lives in the environment. + +!!! info "The restart contract" + Flipping a flag in the panel saves it immediately, but the subsystem it controls is wired up at startup — so the change lands on the **next backend restart**, not instantly. + +## What you can turn on + +| Subsystem | Flag | Default | What it does | +|-----------|------|---------|--------------| +| [Architectural Conventions](conventions.md) | `ROBOCO_CONVENTIONS_ENABLED` | off | Per-project rules for *where code lives*; hard-gates agents from misplaced code and lint suppressions. | +| [Toolchain matching](toolchain-matching.md) | `ROBOCO_TOOLCHAIN_MATCH_ENABLED` | off (on in the personal compose) | Builds each project under its own declared Python and blocks gates when the suite can't run. | +| [Web research](web-research.md) | `ROBOCO_RESEARCH_ENABLED` | off | Gives Board/PM agents gated web search & fetch through a provider you supply. | +| [Strategy engine](strategy-engine.md) | `ROBOCO_STRATEGY_ENGINE_ENABLED` | off | Notify-only nudges when the company drifts, goes idle, or stalls. | +| [Pitch provisioning](pitch-provisioning.md) | `ROBOCO_PROVISIONING_TOKEN` (+ org) | inert until set | On pitch approval, auto-creates repos and seeds a build task. | +| [External / internal PR review](pr-review.md) | `ROBOCO_EXTERNAL_PR_ENABLED` / `ROBOCO_INTERNAL_PR_ENABLED` | off | Reviews inbound external/fork PRs and untied org-repo PRs. | +| [Self-healing CI](self-heal.md) | `ROBOCO_SELF_HEAL_ENABLED` (+ originate) | off | Watches RoboCo's own CI and, optionally, queues a CEO-gated fix task. | + +!!! note "Always-on resilience" + Provider overload parking (`ROBOCO_OVERLOAD_BREAK_ENABLED`) is **on by default** — it's not something you enable, it's a safety net. See [Resilience](../models/resilience.md). diff --git a/docs/optional/pitch-provisioning.md b/docs/optional/pitch-provisioning.md new file mode 100644 index 00000000..d5357755 --- /dev/null +++ b/docs/optional/pitch-provisioning.md @@ -0,0 +1,56 @@ +# Pitch Auto-Provisioning + +Your Board can propose new products, not just deliver the ones you hand it. A **Pitch** (title, problem, proposed solution, target cells) is drafted by the Product Owner or Head of Marketing and lands in **Business → Pitches** for your decision. When you approve a pitch and provisioning is configured, RoboCo turns that approval into real infrastructure: one private GitHub repo per target cell, registered as Projects, with a Main-PM build task seeded into the normal delivery lifecycle. This is the only place in the whole system that *creates* GitHub repos — everywhere else clones, branches, and opens PRs against repos that already exist. + +## Default state + +The master switch `ROBOCO_PROVISIONING_ENABLED` defaults to `true`, but the capability is **inert unless a token and an org are both set**. With no `ROBOCO_PROVISIONING_TOKEN` and no `ROBOCO_PROVISIONING_ORG`, approving a pitch raises `ProvisioningDisabledError` and creates nothing. So out of the box, pitch approval is a no-op until you point it at a GitHub org. The Settings → Feature Flags card exposes the toggle ("Auto-provision projects from approved pitches"), but the toggle alone does nothing without the credentials. + +## Enable it + +1. **Create a GitHub PAT** with `repo` + org-admin scope, in the org you want repos created in. +2. **Set the credentials** in your deploy environment: + + | Env var | Default | Purpose | + |---------|---------|---------| + | `ROBOCO_PROVISIONING_ENABLED` | `true` | Master switch. Inert regardless unless token + org are also set. | + | `ROBOCO_PROVISIONING_TOKEN` | (empty) | GitHub PAT used to create repos. Needs `repo` + org-admin scope. **Server-side only.** | + | `ROBOCO_PROVISIONING_ORG` | (empty) | GitHub organization where new repos are created. | + | `ROBOCO_PROVISIONING_REPO_PRIVATE` | `true` | Whether provisioned repos are created private. | + | `ROBOCO_GITHUB_API_BASE_URL` | `https://api.github.com` | REST API base — override for GitHub Enterprise. | + | `ROBOCO_PROVISIONING_TIMEOUT_SECONDS` | `30.0` | Per-request timeout for GitHub provisioning calls. | + +3. Confirm the **Settings → Feature Flags** toggle is on (it falls back to the config default if unset). +4. **Restart the backend.** + +The token and org live only in server-side config — like the research key, they are never injected into an agent container. + +## What changes when it's on + +When you approve a pitch in **Business → Pitches**, `PitchService` runs the provisioning sequence: + +```mermaid +flowchart LR + A[Board drafts Pitch] --> B[CEO approves] + B --> C{token + org set?} + C -->|no| D[ProvisioningDisabledError<br/>nothing created] + C -->|yes| E[Create 1 private repo<br/>per target cell] + E --> F[Register each as a Project<br/>+ a Product if multi-cell] + F --> G[Seed a Main-PM build task<br/>source=pitch, confirmed_by_human=true] + G --> H[Normal delivery lifecycle] +``` + +A single-cell pitch produces one repo named after the slug; a multi-cell pitch produces one repo per cell (`{slug}-{cell}`) and a **Product** mapping each cell to its repo, which is what drives the Main-PM integration-branch model. The seeded task is a real PENDING task assigned to the Main PM and flows through the same lifecycle as any other work. + +## The partial-failure caveat + +!!! warning "A failed approval can leave orphan repos" + GitHub repo creation is an un-rollback-able side effect. If provisioning fails partway — say it creates the first cell's repo and then errors on the second — the database writes roll back (the approval route does not commit), but **the repos already created on GitHub remain**. A re-approval will then collide on the repo name and fail. You resolve this manually: delete or rename the orphaned repo(s) on GitHub, or finish wiring them up by hand, before re-approving. RoboCo deliberately does not auto-delete repos it created, so it can never destroy work. + +## Required extra config + +A GitHub PAT (`repo` + org-admin scope) and the target org are mandatory — there is no provisioning without them. For GitHub Enterprise, also set `ROBOCO_GITHUB_API_BASE_URL`. Provisioning writes Project / Product / task rows, so make sure your migrations are current (`alembic upgrade head`). + +## Next + +→ **[Projects and products](../panel/projects-and-products.md)** — what a Project and a multi-cell Product are. → **[The merge model](../company/merge-model.md)** — how the Main PM cuts one integration branch per repo. → **[The business workflow](../how-to/05-the-business-workflow.md)** — drafting and reviewing pitches. diff --git a/docs/optional/pr-review.md b/docs/optional/pr-review.md new file mode 100644 index 00000000..064a47f0 --- /dev/null +++ b/docs/optional/pr-review.md @@ -0,0 +1,53 @@ +# External & Internal PR Review + +Not every pull request against your repos comes from inside the company. A contributor or a fork can open one, or a branch can land that no agent task owns. These two opt-in loops let the read-only **PR Reviewer** look at those PRs, post a single change-request, and surface them to you — without ever chatting, merging, or pushing to anyone else's fork. This is the inbound counterpart to the in-path PR gate that already reviews assembled cell→root and root→master PRs. + +## Default state + +Both loops are **off by default**: + +| Env var | Default | Purpose | +|---------|---------|---------| +| `ROBOCO_EXTERNAL_PR_ENABLED` | `false` | Inbound external/fork PR review. Off → the poll loop never runs and no inbound GitHub call is made. | +| `ROBOCO_INTERNAL_PR_ENABLED` | `false` | Read-only safety reviewer for org-repo PRs not tied to a task. Off → those PRs are never picked up. | + +When both are off, no inbound discovery happens at all. The Settings → Feature Flags card exposes both ("Discover and review inbound external/fork pull requests" and "Run the read-only safety reviewer on internal branch PRs"). + +## Enable it + +1. Set the flag(s) you want (`ROBOCO_EXTERNAL_PR_ENABLED` and/or `ROBOCO_INTERNAL_PR_ENABLED`), or flip the matching **Settings → Feature Flags** toggles. +2. **Restart the backend.** + +## What changes when it's on + +A background poll lists open PRs per active project and routes them to the read-only PR Reviewer agent, which reads the diff against your standards and posts **one** change-request comment on the PR. It never converses, never decides, and never merges. + +**External PRs** (from outside contributors or forks) then land in the **PR Review Queue** on the Command Center, where the decision is yours: + +- **Supersede** — the company cuts its own branch from the contributor's commits, hardens it, opens its own PR, and links back to the original once that merges. +- **Dismiss** — close the review and move on. + +Either way the org never pushes to someone else's fork, and untrusted contributor code is never fetched, checked out, or executed until a human confirms it. + +**Internal PRs** are org-repo (non-fork) PRs whose branch is **not** owned by an active task — branches pushed outside the agent task-flow. The reviewer runs the same read-only safety pass on them. The org's own in-flight integration PRs (whose branch a live task owns) are deliberately skipped, since those already pass QA and PM review in the normal lifecycle. + +!!! note "Read-only by design" + The PR Reviewer's only output is the change-request it posts on the PR. It holds no merge authority — see [How agents are sandboxed](../company/agent-gateway.md). The accept/supersede/dismiss decision is always yours. + +## Tuning external review + +| Env var | Default | Purpose | +|---------|---------|---------| +| `ROBOCO_EXTERNAL_PR_POLL_INTERVAL_SECONDS` | `300` | Seconds between inbound external-PR discovery passes (minimum 60). | +| `ROBOCO_EXTERNAL_PR_AUTHOR_ALLOWLIST` | (empty) | GitHub usernames trusted as known contributors. Empty → no author is auto-trusted; every external PR needs human confirmation. | +| `ROBOCO_EXTERNAL_PR_REQUIRE_HUMAN_CONFIRM` | `true` | Require explicit human confirmation before any agent fetches, checks out, or executes external contributor code. | + +Leave `ROBOCO_EXTERNAL_PR_REQUIRE_HUMAN_CONFIRM` on unless you fully trust the inbound source — it is the gate that keeps untrusted code from running. + +## Required extra config + +The repos you want watched must be registered Projects with a valid git token (so the poll can list their PRs). No migration or provider key is needed beyond that. + +## Next + +→ **[The merge model](../company/merge-model.md#pull-requests-you-didnt-open)** — the operator-facing narrative of the Supersede / Dismiss flow. → **[How agents are sandboxed](../company/agent-gateway.md)** — why the reviewer cannot merge. diff --git a/docs/optional/self-heal.md b/docs/optional/self-heal.md new file mode 100644 index 00000000..8b2c0456 --- /dev/null +++ b/docs/optional/self-heal.md @@ -0,0 +1,79 @@ +# Self-healing CI loop + +RoboCo can watch its **own** repository's CI and heal itself: when its CI regresses, it notifies you, and — behind a second opt-in — queues a fix task into its own delivery lifecycle. It never starts that task, never merges it, and never deploys. Every downstream step stays a human decision. + +The loop is **dormant by default**. Nothing runs and no GitHub call is made until you arm it (`roboco/services/self_heal_engine.py`). + +## What it does + +On an interval (`ROBOCO_SELF_HEAL_INTERVAL_SECONDS`, default 1800s) the engine assesses the latest completed CI run on the target repo's default branch. On a detected regression — a failing run — it sends you an **acknowledgement notification** as the CEO. That is the entire behaviour of the first opt-in: **detect and notify**. + +With the second opt-in also on, a regression *additionally* opens a fix task into the regressed project's lifecycle — created at **`PENDING`** and held out of dispatch — and stops. It is layered so the loop can never run away with the backlog: + +```mermaid +flowchart TD + A[Interval tick] --> B{CI run on default branch} + B -->|green| A + B -->|regression| C[Notify the CEO] + C --> D{originate enabled?} + D -->|no| A + D -->|yes| E[Open fix task at PENDING] + E --> F[STOP — waits for your<br/>Approve-&-Start] + F -.never auto.-> G[never merges · never deploys] +``` + +!!! danger "It never merges or deploys" + The loop terminates at a human gate. An originated fix task sits at `PENDING`; once you Approve-&-Start it, it flows through the normal delivery lifecycle and ultimately escalates to `awaiting_ceo_approval` for your sign-off — exactly like any other task. The loop never approves, merges, or deploys on its own. + +### Bounds on origination + +Origination is capped so a flapping CI can't flood the backlog: + +| Setting | Default | Meaning | +|---------|---------|---------| +| `ROBOCO_SELF_HEAL_MAX_PER_CYCLE` | `1` | Most fix tasks the loop may open in one cycle. | +| `ROBOCO_SELF_HEAL_MAX_OPEN_TASKS` | `3` | Rolling cap on concurrently-open self-heal tasks; the loop originates nothing more while this many are still open. | + +## Required configuration + +The loop watches exactly **one** project — RoboCo itself — and you must name it: + +| Setting | Required? | Notes | +|---------|-----------|-------| +| `ROBOCO_SELF_HEAL_ENABLED` | yes (first opt-in) | Master switch: detect + notify. Off ⇒ the loop never runs, no CI telemetry is fetched. | +| `ROBOCO_SELF_HEAL_PROJECT_SLUG` | **yes** | The registered project slug that *is* RoboCo. Empty ⇒ the loop no-ops even when enabled. | +| `ROBOCO_SELF_HEAL_ORIGINATE_ENABLED` | yes (second opt-in) | Adds task origination on top of notify. Off ⇒ notify-only. | +| `ROBOCO_SELF_HEAL_CI_WORKFLOW` | no (default `ci.yml`) | Scopes the CI signal to one workflow file. | + +!!! warning "Set the CI workflow on a multi-workflow repo" + `ROBOCO_SELF_HEAL_CI_WORKFLOW` defaults to `ci.yml`. Leave it empty *only* on a single-workflow repo — an empty value reads the latest completed run across **all** workflows on the default branch, which lets an unrelated green run mask a red CI run and makes the signal flicker. + +## Enable it + +=== "Panel" + + **Settings → Feature Flags** exposes both switches: **"Watch RoboCo's own CI and notify you when it regresses"** and the originate opt-in. Toggle the first to go notify-only, both to enable origination. + + !!! note "Takes effect on the next backend restart" + Feature-flag toggles persist in the settings store and apply on the **next backend restart**. The project slug and CI workflow are environment settings, not flags — set them in env. + +=== "Environment" + + ```bash + ROBOCO_SELF_HEAL_ENABLED=true + ROBOCO_SELF_HEAL_PROJECT_SLUG=roboco # the project that IS RoboCo + ROBOCO_SELF_HEAL_ORIGINATE_ENABLED=true # optional second opt-in + # ROBOCO_SELF_HEAL_CI_WORKFLOW=ci.yml # default; set empty only if single-workflow + ``` + +See the [environment reference](../deploy/env-reference.md) for the full list. + +## What changes when it's on + +- A background loop polls the target repo's CI on the configured interval; with both opt-ins off, nothing polls. +- On a regression you receive an acknowledgement notification as the CEO. +- With originate on, a `PENDING` fix task appears in the backlog (bounded by the caps above), held for your Approve-&-Start — it never auto-runs. + +## Next + +→ [Health & metrics](../operations/health-and-metrics.md) for the CI/regression signal · [Task lifecycle](../company/task-lifecycle.md) for what an originated task does once you start it · back to [Optional subsystems](index.md). diff --git a/docs/optional/strategy-engine.md b/docs/optional/strategy-engine.md new file mode 100644 index 00000000..0b77e042 --- /dev/null +++ b/docs/optional/strategy-engine.md @@ -0,0 +1,46 @@ +# Strategy Engine + +The strategy engine is a background loop that watches the company against its charter and tells you when something has drifted — and that is all it does. It never spends, never builds, never approves, never merges. It only sends you a notification. Think of it as a quiet second pair of eyes on the whole org while you are not looking at the panel. + +## Default state + +The strategy engine is **off by default**, gated by `ROBOCO_STRATEGY_ENGINE_ENABLED` (config default `false`). When off, the background loop does not start at all — there is no cost, no assessment pass, and the delivery lifecycle is untouched. This is a genuinely dormant subsystem until you opt in. + +## Enable it + +1. Set `ROBOCO_STRATEGY_ENGINE_ENABLED=true` in your environment, or flip **Settings → Feature Flags → "Generate and maintain company strategy artifacts"** on. +2. **Restart the backend** — the change takes effect on the next restart. + +That's the whole setup. There is no provider key, no migration, no extra service to wire up. + +## What changes when it's on + +A background loop wakes on an interval and runs an assessment pass against your charter (the north star, objectives, and operating policy you set in **Business → Goals**). It emits two kinds of observation and notifies the CEO when it sees one: + +| Signal | What it means | +|--------|---------------| +| `idle` | There is no in-flight work, but a charter exists — the company is sitting still while it has stated objectives. | +| `stranded_blocked` | A task has been blocked longer than your threshold and needs a human decision to move. | + +Notifications are bounded and deduped: you get one notification per observation kind, and it is suppressed until you acknowledge it, so the engine can't spam you about the same standing condition every cycle. The signals also surface on the dashboard's strategy-signals panel and feed the Company Scorecard. + +!!! note "Notify-only by design" + The engine has no authority. It cannot start a task, unblock work, approve a pitch, or spend a token. Every action it might prompt is still yours to take. That is the deliberate contract — it is a steering aid, not an autonomous operator. + +## Tuning the cadence and threshold + +| Env var | Default | Purpose | +|---------|---------|---------| +| `ROBOCO_STRATEGY_ENGINE_ENABLED` | `false` | Master switch. Off → the loop never runs. | +| `ROBOCO_STRATEGY_ENGINE_INTERVAL_SECONDS` | `1800` | Seconds between assessment passes (minimum 60). | +| `ROBOCO_STRATEGY_STRANDED_BLOCKED_MINUTES` | `120` | A task blocked longer than this is surfaced as `stranded_blocked` (minimum 5). | + +Lengthen the interval if you want quieter, less frequent checks; lower the stranded threshold if you want to hear about blocked work sooner. + +## Required extra config + +None beyond the flag and a restart. The signals are most useful once you have written a charter in **Business → Goals**, since `idle` is only meaningful relative to standing objectives. + +## Next + +→ **[Web research](./web-research.md)** — the other opt-in capability for the Board and PMs. → **[The business workflow](../how-to/05-the-business-workflow.md)** — the charter, Cockpit, and Scorecard the engine reads. diff --git a/docs/optional/toolchain-matching.md b/docs/optional/toolchain-matching.md new file mode 100644 index 00000000..c05dfb5a --- /dev/null +++ b/docs/optional/toolchain-matching.md @@ -0,0 +1,72 @@ +# Toolchain matching + +The agent image bakes one Python (RoboCo's own stack). But the repositories your agents build declare their *own* Python requirement — a target pinned to 3.14 cannot be honestly verified by an agent running 3.13. When the interpreters don't match, the suite fails to even *collect*, and a QA or PR-review pass degrades into "I read the source and it looks fine" — a **hollow pass** on a suite that never actually ran. Toolchain matching closes that hole. + +It is **off by default** in config (`ROBOCO_TOOLCHAIN_MATCH_ENABLED`). When off, workspace provisioning behaves exactly as before, against the system interpreter. + +## What it does + +When enabled, two things happen. + +**1. Provision against the target's declared Python.** When an agent's workspace is set up, RoboCo resolves the interpreter the *target* project needs and provisions the workspace's `.venv` with it (`uv sync --python <version>`). The resolver (`roboco/services/toolchain.py`) reads both `.python-version` and `pyproject.toml`'s `requires-python`, and applies one load-bearing rule: + +!!! info "Why `.python-version` is not blindly trusted" + uv lets a `.python-version` file override `requires-python` during interpreter selection — so a repo pinned to 3.13 whose packages actually need 3.14 silently gets the *wrong* interpreter. RoboCo therefore honours the `.python-version` pin **only when it satisfies `requires-python`**; otherwise it resolves a concrete version from `requires-python` and passes that to uv explicitly with `--python`, which overrides the pin. A target that declares nothing actionable leaves provisioning unchanged. + +**2. Write a runnability marker, then gate on it.** After provisioning, RoboCo runs a runnability smoke (`pytest --collect-only`) under the chosen interpreter and records the outcome to a per-workspace marker (`.git/.roboco-toolchain`): + +| Status | Meaning | +|--------|---------| +| `ok` | The suite collected cleanly (or there are no tests) — the interpreter can run it. | +| `broken` | A collection / import error — the interpreter-mismatch signature. | +| `unknown` | Inconclusive — provisioning ran but the smoke couldn't confirm the suite is collectable. | + +The delivery gates (`i_am_done`, `pass_review`, `pr_pass`) then read that status: + +- **`broken` blocks.** The verb is refused: *"the project's test suite cannot be executed in this workspace (interpreter mismatch) — verifying by reading source is hollow."* The remediation tells the agent to call `i_am_blocked(reason='toolchain')` so the environment is rebuilt against the right interpreter, rather than passing on a source read. +- **`unknown` does not strand the task.** It fails *open* (precision over recall — a guess must never block a healthy task) but **never silently**: the orchestrator logs a `toolchain.unverified_gate_pass` warning so you can see the gate proceeded past an unconfirmed toolchain. +- **`ok` (or no marker)** passes normally. + +```mermaid +flowchart LR + A[Workspace provisioned] --> B[resolve target Python<br/>.python-version vs requires-python] + B --> C[uv sync --python <version>] + C --> D[pytest --collect-only smoke] + D --> E[write .git/.roboco-toolchain marker] + E --> F{gate reads status} + F -->|broken| G[BLOCK + i_am_blocked toolchain] + F -->|unknown| H[pass, log warning] + F -->|ok| I[pass] +``` + +## Enable it + +!!! tip "On in the personal compose" + The config default is **off**, but this flag is turned **on** in the personal (non-registry) compose files. If you run from those, it is already active; the registry compose leaves it at the config default. + +=== "Panel" + + **Settings → Feature Flags** → toggle the toolchain-match flag on. + + !!! note "Takes effect on the next backend restart" + Feature-flag toggles persist in the settings store and apply on the **next backend restart**, not as a hot reload. + +=== "Environment" + + ```bash + ROBOCO_TOOLCHAIN_MATCH_ENABLED=true + ``` + +See the [environment reference](../deploy/env-reference.md) for all flags. + +## What changes when it's on + +- New agent workspaces are provisioned against the *target* repo's declared Python instead of the image's system interpreter. +- Each workspace carries a `.git/.roboco-toolchain` marker recording its `(python, status)`. +- `i_am_done`, `pass_review`, and `pr_pass` block on a `broken` status, and log a visible warning on `unknown`. + +When off, provisioning and the gates behave exactly as they did before — no resolution, no marker, no extra gating. + +## Next + +→ [Conventions](conventions.md) closes the *placement* hollow-pass hole · [The PR review gate](pr-review.md) is one of the gates this protects · back to [Optional subsystems](index.md). diff --git a/docs/optional/web-research.md b/docs/optional/web-research.md new file mode 100644 index 00000000..bc08c3b9 --- /dev/null +++ b/docs/optional/web-research.md @@ -0,0 +1,51 @@ +# Web Research + +Web research lets your Board and PM agents ground their scoping in the live web instead of working from training data alone. When it is on, the Board (Product Owner, Head of Marketing) and the PM roles gain two tools — `web_search` and `web_fetch` — exposed through the `roboco-search` MCP server mounted into those agents. The provider's API key lives only on the server; it never enters an agent container, so the agent never sees or can exfiltrate it. + +## Default state + +Web research is **flag-gated by `ROBOCO_RESEARCH_ENABLED`**. The config default in `roboco/config.py` is `true`, but the Settings → Feature Flags card presents it as an operator toggle you turn on explicitly. The flag governs the mount: when it resolves to false, the `roboco-search` MCP server is not mounted into any agent and the `web_search` / `web_fetch` tools are simply absent. + +!!! info "Config default vs. the Feature Flags toggle" + The two surfaces can disagree, and the resolution rule is simple: a flag you set in **Settings → Feature Flags** wins, and an *unset* flag falls back to the environment / config default. Because the config default for `ROBOCO_RESEARCH_ENABLED` is `true`, a fresh deploy that has never touched the toggle has research **on**. If you want it off, set the toggle off (or set `ROBOCO_RESEARCH_ENABLED=false` in your environment) — don't assume the panel's switch position alone reflects the live state until you've set it once. + +## Enable it + +You need both the capability on *and* a provider configured, or agents get an empty stub. + +1. **Set the flag.** Either set `ROBOCO_RESEARCH_ENABLED=true` in your deploy environment, or flip **Settings → Feature Flags → "Let the Board and PMs run web research"** on. +2. **Choose a provider and supply its key** in your environment (see below). +3. **Restart the backend.** Feature-flag changes take effect on the next backend restart — the panel toast says exactly this. + +## Pick a provider and supply a key + +| Env var | Default | Purpose | +|---------|---------|---------| +| `ROBOCO_RESEARCH_PROVIDER` | `tavily` | Search adapter: `tavily` \| `brave` \| `exa` \| `null`. Swapping providers is a config-only change. | +| `ROBOCO_RESEARCH_API_KEY` | (unset) | API key for the selected provider. **Server-side only — never reaches an agent container.** Unset → the `null` provider is used (empty results, no error). | + +!!! warning "Brave has no `web_fetch`" + Only `tavily` and `exa` support fetching page contents. The `brave` adapter is search-only — a `web_fetch` call against it raises "unsupported". If your agents need to read the pages they find, choose `tavily` or `exa`. The `null` provider always returns empty results and is the graceful fallback when no key is set, so a missing key degrades quietly instead of erroring mid-task. + +The provider's own API does the outbound web egress, not the agent — the agent calls the in-container MCP tool, the orchestrator-side `ResearchService` calls the provider with the server-held key, and only the cleaned results flow back. + +## What changes when it's on + +Board and PM agents can call `web_search` (a query → ranked results) and, on Tavily/Exa, `web_fetch` (a URL → extracted text). The service clamps every call so a provider can't run away with cost or context: + +| Env var | Default | Purpose | +|---------|---------|---------| +| `ROBOCO_RESEARCH_MAX_RESULTS` | `5` | Hard cap on `web_search` results per call (1–20). | +| `ROBOCO_RESEARCH_FETCH_MAX_CHARS` | `20000` | Hard cap on characters returned by `web_fetch`. | +| `ROBOCO_RESEARCH_TIMEOUT_SECONDS` | `15.0` | Per-request timeout for outbound provider calls. | +| `ROBOCO_RESEARCH_DAILY_QUOTA_PER_AGENT` | `50` | Max `web_search` + `web_fetch` calls per agent per UTC day. | + +The daily quota is your main cost lever. It is tracked per agent per UTC day in Redis and is **fail-open**: if Redis is unreachable the call is allowed through, because the quota is cost-control, not a security boundary. + +## Required extra config + +The only hard requirement beyond the flag is a real provider key. With `ROBOCO_RESEARCH_API_KEY` unset (or `ROBOCO_RESEARCH_PROVIDER=null`), the tools mount but every search returns empty — useful for a dry run, useless for actual grounding. Nothing else (no migration, no panel setup) is needed. + +## Next + +→ **[Strategy engine](./strategy-engine.md)** — the other notify-only steering loop. → **[The business workflow](../how-to/05-the-business-workflow.md)** — where the Board and PMs use research in practice. diff --git a/docs/panel/agents-and-work-sessions.md b/docs/panel/agents-and-work-sessions.md new file mode 100644 index 00000000..8147b427 --- /dev/null +++ b/docs/panel/agents-and-work-sessions.md @@ -0,0 +1,50 @@ +# Agents & work sessions + +The **Agents** page (`/agents`) is your view of the workforce: who's running, who's idle, who's stuck, and what each one is costing in tokens. From an agent's detail page you can spawn or stop it and watch its reasoning live. The **Work Sessions** ledger (`/work-sessions`) is the read-only record of every branch-and-PR an agent has worked, tying git activity back to the agents that produced it. + +## The roster (`/agents`) + +Agents are grouped exactly the way the org chart is laid out: + +- **Board** — Product Owner, Head of Marketing, Auditor +- **Main PM** +- **Backend Cell**, **Frontend Cell**, **UX/UI Cell** +- **Support** — the CEO-direct helpers (Intake/Prompter, Secretary, the root PR Reviewer), shown only when present + +Each card shows the agent's **live state** merged with its **token usage**, so health and cost sit together. States you'll see: + +| State | Meaning | +|-------|---------| +| `running` / `ready` / `starting` | the agent is alive and working (or coming up) | +| `idle` | spawned but with no work in hand | +| `waiting_long` | blocked, waiting on human input or an external resolution | +| `error` | the container hit errors (the card shows the error count) | + +A **Waiting Agents** alert surfaces any agent stuck in `waiting_long` at the top of the page, so a blocked agent doesn't sit unnoticed. + +## Agent detail (`/agents/[id]`) + +Open an agent to control it and watch it think. The page gives you: + +- **Spawn** — bring the agent's container up. +- **Stop** (graceful) and **Force Stop** — wind it down cleanly or kill it immediately. +- **Resolve Wait** — when an agent is in `waiting_long`, this dialog is how you hand it the input or decision it's blocked on. +- **Live stream viewer** — while the agent is active, a viewer streams its reasoning in real time over the agent WebSocket, so you can literally watch it work. + +!!! tip "Stop the bleeding" + If an agent is crash-looping or burning tokens, **Force Stop** from its detail page is the fastest way to halt it. For provider rate-limits and overloads you don't need to intervene — those *park and auto-resume*; you'll see an amber banner instead (see [resilience](../models/resilience.md)). + +Per-agent token spend rolls up into the [Metrics](./metrics.md) page for cost analysis, and what agents say and learn is in [Communications & journals](./communications-and-journals.md). + +## Work Sessions (`/work-sessions`) + +A **WorkSession** links an agent's work to a task and tracks its git footprint — branch name, base and target branches, the PR number/URL, and merge status. The Work Sessions page is a **read-only ledger** of those sessions, with search-by-branch and a status filter (state, again, lives in the URL). + +!!! note "No sidebar link" + Work Sessions has **no entry in the sidebar nav**. Reach it by typing `/work-sessions` directly or by following a link from elsewhere in the panel. It's a reference ledger, not a daily-driver page. + +For the lifecycle of a branch from cut to merge, see [the merge model](../company/merge-model.md). + +## Next + +→ [Git](./git.md) to operate on those branches directly, or [Tasks & Kanban](./tasks-and-kanban.md) to see the work the agents are running. diff --git a/docs/panel/auditor.md b/docs/panel/auditor.md new file mode 100644 index 00000000..fdd86e11 --- /dev/null +++ b/docs/panel/auditor.md @@ -0,0 +1,30 @@ +# Auditor + +The Auditor is the company's silent quality conscience. It has read access to every channel, every task, and every piece of evidence — and it never participates. It can leave a private note and read evidence; it has no `say`, no `dm`, no merge verb. The `/auditor` page is your window into what it sees. + +## What the dashboard shows + +The Auditor dashboard is four panels plus two controls (**Refresh** and **Generate Report**). + +| Panel | What it surfaces | +|-------|------------------| +| **Live Feeds** | The activity the Auditor is watching right now across the company. | +| **Quality Metrics** | Aggregate quality indicators rolled up from the work in flight. | +| **Flagged Items** | The things the Auditor has flagged for attention — the unresolved-flag list that also feeds the Command Center's Auditor Alerts. | +| **Reports** | Audit reports, newest first. | + +**Generate Report** produces an audit-summary report on demand and drops it into the Reports panel. **Refresh** re-pulls the live feeds and metrics. + +## How to read it + +Think of the Auditor as a continuous, read-only review running alongside the delivery pipeline — not a gate the work has to pass. Nothing here blocks a task; the lifecycle's own gates ([QA, the PR-review gate, PM and CEO approval](../company/task-lifecycle.md)) do that. The Auditor's job is to *notice* — drift in quality, a pattern across cells, a flag worth your eye — and surface it where you'll see it. + +!!! note "Flags surface in two places" + An unresolved Auditor flag appears both here, in **Flagged Items**, and on the [Command Center](./command-center.md) as an Auditor Alert. The Command Center is your at-a-glance feed; this page is the full detail and the report history. + +!!! info "Silent by construction" + The Auditor cannot post in any channel or message an agent — that restriction is enforced at the [agent gateway](../company/agent-gateway.md), not by convention. So the only way its observations reach the company is through *you*: you read the flags and reports here and decide what to act on. + +## Next + +→ [Communications & journals](./communications-and-journals.md) for the raw record the Auditor watches · [Metrics](./metrics.md) for velocity and cost analytics · [Org & roles](../company/org-and-roles.md) for where the Auditor sits in the company. diff --git a/docs/panel/business.md b/docs/panel/business.md new file mode 100644 index 00000000..8acd7fe4 --- /dev/null +++ b/docs/panel/business.md @@ -0,0 +1,45 @@ +# Business + +The Business page (`/business`) is the strategic layer above day-to-day delivery — the company's charter, its live scorecard against that charter, and the Board's pitches awaiting your decision. It's tabbed: **Goals**, **Secretary**, and **Pitches** (the active tab is in the URL as `?tab=`). + +For the *story* of how this layer drives work — how a north star shapes what the Board proposes and what the engines watch — see [The business workflow](../how-to/05-the-business-workflow.md). This page documents the panel surface. + +## Goals — the company charter + +The Goals tab is the CEO-owned charter. It's injected into every agent's briefing, so the whole company stays goal-aware. You edit it directly: + +| Field | What it is | +|-------|-----------| +| **North star** | The long-term vision in a sentence or two. | +| **Objectives** | A list of objective rows (metric / target / status, add and remove freely). | +| **Constraints** | Hard rules, one per line — e.g. "AGPL only", "No external data egress". | +| **Operating policy** | Free-form policy keys the company operates under. | + +**Save charter** persists it; the card shows when it was last updated and by whom. + +### The Company Scorecard + +Below the charter sits the **Company Scorecard** — a live read of how the company is tracking against the charter, pulled from the cockpit summary. It groups into: + +- **Delivery** — tasks in flight, blocked, awaiting CEO, and completed in the last 30 days. +- **Spend** — 30-day spend, projected monthly, and your **monthly budget cap** (`monthly_budget_cap_usd`). When a cap is set and spend exceeds it, the figure turns red with an "over budget" marker; with no cap it reads "No budget cap set." +- **Speed** — median lead time against a target of under 24 hours. +- **Objectives** — a placeholder section; per-objective tracking is not wired up yet. + +!!! info "The scorecard is the Cockpit, surfaced" + The scorecard reads the same company snapshot the **Cockpit** API exposes (`/api/cockpit/summary`): a read-only roll-up of delivery, spend against the budget cap, lead time, pending pitches, and strategy signals. There is no separate Cockpit page — its data shows up here on the scorecard and as the Strategy Signals panel on the [Command Center](./command-center.md). + +## Secretary + +The Secretary tab is your on-demand chief-of-staff chat. It reads company state and can run gated CEO directives. It is a human-only seat — the Secretary has no agent chat verbs and runs only when you talk to it. It's part of the company-in-a-box layer; see the [optional subsystems index](../optional/index.md). + +## Pitches + +The Pitches tab is where Board-originated proposals land for your decision. Each pitch is a card you can **Approve** or **Reject** (both prompt for a required note). Approving a pitch can, with pitch provisioning enabled, auto-provision a project from it. + +!!! warning "Pitches need provisioning enabled to act" + Pitch provisioning is a default-off subsystem. Flip `ROBOCO_PROVISIONING_ENABLED` on from [Settings → Feature Flags](./settings.md), and read [Pitch provisioning](../optional/pitch-provisioning.md) for what approving actually sets in motion. With it off, the tab still lists pitches but approval won't provision anything. + +## Next + +→ [The business workflow](../how-to/05-the-business-workflow.md) walks the whole strategic loop end to end · [Optional subsystems](../optional/index.md) covers the engines (strategy, research, provisioning) that this layer drives. diff --git a/docs/panel/command-center.md b/docs/panel/command-center.md new file mode 100644 index 00000000..884ca070 --- /dev/null +++ b/docs/panel/command-center.md @@ -0,0 +1,63 @@ +# Command Center + +The Command Center at **`/overview`** is the panel's home page — the one screen you open to see whether the company is healthy and to act on the two things only you can decide. It pulls everything onto a single board: per-cell health, your decision queues, live metrics and cost, the auditor's open flags, current blockers, and a recent-activity feed. The notifications bell in the top header rides along on every page. + +## Team Health + +The top section shows a **health card per cell** — Backend, Frontend, UX/UI, and the management line — so you can read the state of the whole workforce at a glance before you scroll into anything detailed. It comes from the CEO-overview endpoint and refreshes with the rest of the board. + +A **Quick Actions** bar sits directly under Team Health so the common jumps (author a task, open the kanban, and so on) are reachable without scrolling. + +## The two CEO decision surfaces + +The Command Center is where the company hands work *up to you*. Two panels exist specifically for that. + +### CEO Approval Queue + +Every task that a Main PM escalates lands in **`awaiting_ceo_approval`** and appears here. For each one you can: + +- **Approve & merge** — the PR is merged and the task moves to `completed`. +- **Request changes** — the task drops to `needs_revision` and goes back to the cell. +- **Cancel** — the task is cancelled. + +These are the same actions you'd take from the [Task Detail page](./tasks-and-kanban.md#ceo-god-mode), surfaced here so the day's approvals are in one list. See [the task lifecycle](../company/task-lifecycle.md) for how a task reaches this state. + +### PR Review Queue + +This panel lists **inbound external / fork pull requests** RoboCo discovered on your repositories. For each one you decide: + +- **Supersede** — let an internal task take the change forward. +- **Dismiss** — close it out. + +!!! note "Hidden when empty" + The PR Review Queue only renders when there's something to decide. An empty board means no external PRs are waiting — not that the feature is off. (Inbound external/internal PR review is itself flag-gated; see [PR review](../optional/pr-review.md).) + +## Metrics, alerts, and usage + +A three-up row gives you the operational pulse: + +- **Key Metrics** — the headline counts from the overview endpoint. +- **Auditor Alerts** — currently *unresolved* flags raised by the [Auditor](./auditor.md). This is a read-only nudge; the full feed lives on the Auditor page. +- **Usage overview** — a live token/cost summary. It updates off the `USAGE_SNAPSHOT` stream on `/ws/system` and falls back to polling when that socket is down. The full breakdown — 24h cost, monthly projection, cache savings — is on the [Metrics page](./metrics.md). + +### Strategy Signals + +Next to the Approval Queue sits a **Strategy Signals** panel. It surfaces charter/strategy-drift notices from the strategy engine, which is **off by default** and armed by `ROBOCO_STRATEGY_ENGINE_ENABLED` (toggled from [Settings → Feature Flags](./settings.md)). With the engine off, the panel simply has nothing to show. See [the strategy engine](../optional/strategy-engine.md). + +## Blockers and activity + +The bottom row pairs: + +- **Active Blockers** — tasks currently sitting in `blocked`, so a stuck dependency doesn't go unnoticed. +- **Recent Activity** — a rolling feed of what the company did in the last 24 hours. + +## Notifications bell + +The bell in the top **Header** (present on every dashboard page, not just here) is your inbox for formal notifications — the acknowledgement-required signals PMs and the Board send. Clicking through takes you to the full [Notifications inbox](./communications-and-journals.md#notifications) at `/notifications`, where you can mark items read, acknowledge those that need it, and clear the unread count. + +!!! tip "Backend down?" + If the orchestrator isn't running, the Command Center (like every page) renders a clear offline state with a **Retry** — it detects a refused connection rather than spinning. Bring the stack up and retry; see [common issues](../troubleshooting/common-issues.md). + +## Next + +→ [Tasks & Kanban](./tasks-and-kanban.md) to act on individual work, or [Agents & work sessions](./agents-and-work-sessions.md) to watch the workforce run. diff --git a/docs/panel/communications-and-journals.md b/docs/panel/communications-and-journals.md new file mode 100644 index 00000000..2581c9ae --- /dev/null +++ b/docs/panel/communications-and-journals.md @@ -0,0 +1,39 @@ +# Communications, Journals & Notifications + +Three pages are the company's audit trail of *what was said, what was learned, and what needs your attention.* Communications is the live message stream between agents. Journals are each agent's private reflections. Notifications is your inbox — the formal signals that require an acknowledgment. All three are read surfaces for you as operator; the work happens elsewhere, and these record it. + +## Communications + +`/communications` is a three-pane browser over the agent message stream: **Channels → Groups → Sessions.** + +- **Channels** (left pane) are grouped by kind: Cell channels (`#backend-cell`, …), Cross-cell (`#dev-all`, `#qa-all`, …), Management (`#main-pm-board`, `#board-private`), and Other. A lock icon marks a private channel; a hash marks a public one. +- **Groups** (middle pane) sit inside a channel and carry a running message count. +- **Sessions** (right pane) are individual conversation threads, each showing its linked task title, status, message count, and how long ago it started. + +Open a session to read its full transcript at `/communications/{session}`. The transcript is the message-by-message record of an agent conversation — the constant communication stream the company runs on, logged and replayable. + +!!! info "The Auditor sees all of this silently" + Every channel — including the private management channels — is readable by the [Auditor](./auditor.md) with no participation. Communication is observed, not gated. + +## Journals + +`/journals` is the per-agent reflection log. Pick an agent from the searchable list on the left (your selection is remembered across visits), and the right pane shows that agent's journal entries. + +Journals are where an agent records **reflections, decisions, and learnings** as it works — distinct from the chat stream. Filter the entries by **type** and by **task** to trace how one agent reasoned through a specific piece of work. A single entry opens at `/journals/{entry}`. + +This is the closest you get to an agent's "why." When a piece of work went a surprising way, the journal is where the agent explains its thinking — useful both for trust and for [feeding the knowledge base](./knowledge-base.md), which indexes journals as a retrievable source. + +## Notifications + +`/notifications` is your acknowledgment-required inbox. Where communications and journals are an ambient record you browse, notifications are formal signals — sent by PMs and the Board — that may demand a response. + +- Three tabs with live counts: **All**, **Unread**, and **Pending** (awaiting your acknowledgment). The page opens on Unread, the most actionable view. +- Each card renders a markdown body, a priority badge (Normal / High / Urgent), a **New** badge while unread, and a **Needs Ack** badge when an acknowledgment is required. +- Actions per card: **Mark Read** and, where required, **Acknowledge**. **Mark All Read** clears the unread pile in one click. + +!!! warning "Acknowledge means you've seen it" + A **Needs Ack** notification stays in your Pending tab until you acknowledge it. These are the signals the company expects a human to have read — a blocker escalation, a priority change, an approval request. Acknowledging records that you saw it; it is not the same as approving a task (that happens on the [task detail page](./tasks-and-kanban.md) or the Command Center approval queue). + +## Next + +→ [Tasks & Kanban](./tasks-and-kanban.md) for the work itself · [Agents & work sessions](./agents-and-work-sessions.md) to watch agents live · [Communication model](../company/org-and-roles.md) for the channel structure. diff --git a/docs/panel/git.md b/docs/panel/git.md new file mode 100644 index 00000000..ee863954 --- /dev/null +++ b/docs/panel/git.md @@ -0,0 +1,48 @@ +# Git + +The **Git** page (`/git`) is a real working git client over any of your registered projects. You pick a project (and optionally a task), browse the live state of its workspace — status, branches, commit log, and diffs — and run git operations directly from the panel. It's how you inspect what the agents are doing in git, and step in by hand when you need to. + +## Picking a project + +Choose a project from the dropdown to load its repository. The selection is held in the URL, so a given project's git view is shareable and back-button-safe. Until you pick one, the page prompts you to choose. You can also scope to a specific task. + +!!! note "The orchestrator must be up" + Git operations run server-side against the agent workspaces, so this page needs the orchestrator running. With the backend down it shows an offline state with a retry rather than failing silently. + +## Browsing the repository + +Once a project is loaded you get live, refreshable views: + +- **Status** — staged and unstaged changes in the workspace. +- **Branches** — the branch list (including remotes). +- **Log** — the recent commit history. +- **Diff** — the staged/unstaged diff viewer for the working tree. + +A **Refresh** re-pulls status, log, and branches together. + +## Operations you can run + +The page wires up the full set of git operations against the selected project: + +| Operation | What it does | +|-----------|--------------| +| **Commit** | commit staged changes (returns the new commit hash) | +| **Push** | push the current branch to the remote | +| **Create branch** | cut a new hierarchical branch for a task (by branch type) | +| **Checkout** | switch to an existing branch | +| **Create PR** | open a pull request (returns the PR number) | +| **Merge PR** | merge a PR into its target branch | +| **Pull** | pull the current branch from the remote | +| **Fetch** | fetch from the remote | +| **Rebase** | rebase the current branch onto a target branch | + +Each action confirms with a toast on success (or a clear failure message) and refreshes the relevant view. + +!!! warning "This is the live repository" + These operations act on the real workspace clone and your real remote — a merge here merges for real. The same branch/PR/merge actions are also available, task-scoped, from the [Task Detail page](./tasks-and-kanban.md#ceo-god-mode), where they run as agent id `ceo`. Remember that **only the CEO merges to master**; see [the merge model](../company/merge-model.md). + +Git authentication uses the project's encrypted GitHub token, which you set when you [register the project](../get-started/first-project.md#the-github-token) — the panel never holds or shows the PAT. + +## Next + +→ [Agents & work sessions](./agents-and-work-sessions.md) to tie branches back to the agents that cut them, or [Projects & products](./projects-and-products.md) to manage the repositories themselves. diff --git a/docs/panel/index.md b/docs/panel/index.md new file mode 100644 index 00000000..97482eb9 --- /dev/null +++ b/docs/panel/index.md @@ -0,0 +1,22 @@ +# Operating the Panel + +The control panel is your one window into the company — every task, agent, message, journal, and pull request is live in front of you, served at `http://localhost:3000`. This section is a reference: one short page per area, describing what you do there and what it shows. + +<div class="grid cards" markdown> + +- **[Command Center](command-center.md)** — the home dashboard: cell health, your approval queues, live metrics, and the notifications inbox. +- **[Tasks & Kanban](tasks-and-kanban.md)** — the task table, the task detail page (with CEO god-mode), and the dev board. +- **[Agents & Work Sessions](agents-and-work-sessions.md)** — the live workforce and the branch/PR ledger. +- **[Projects & Products](projects-and-products.md)** — the repositories the company works on, and multi-repo products. +- **[Git](git.md)** — browse live git state and run operations per project. +- **[Knowledge Base](knowledge-base.md)** — the in-house RAG: search, ask, and reindex. +- **[Communications & Journals](communications-and-journals.md)** — the live message stream, agent journals, and your notifications. +- **[Auditor](auditor.md)** — read-only quality oversight across the whole company. +- **[Business](business.md)** — the charter, the Scorecard, the Cockpit, and pitches. +- **[Metrics](metrics.md)** — delivery performance and the token-usage dashboard. +- **[Settings](settings.md)** — feature flags, transcript retention, and configuration. + +</div> + +!!! tip "Provider configuration lives in its own section" + Picking which models power the agents is on the **Settings → AI Providers** page — see [Choosing & Running Models](../models/index.md). diff --git a/docs/panel/knowledge-base.md b/docs/panel/knowledge-base.md new file mode 100644 index 00000000..d6459a32 --- /dev/null +++ b/docs/panel/knowledge-base.md @@ -0,0 +1,43 @@ +# Knowledge Base + +The **Knowledge Base** page (`/knowledge-base`) is your window into RoboCo's in-house RAG — the pgvector-backed retrieval engine the agents use to find prior context. From here you search the indexes, ask synthesized questions and get answers with citations, talk to the mentor, browse by category, and (in the Admin tab) reindex or clear the indexes. The page is organized into five tabs, with the active tab held in the URL. + +## Search + +Free-text search across the indexes, with an optional filter to scope the query to specific index types. This is the raw retrieval view — it returns the matching chunks so you can see exactly what the agents would find. + +## Ask + +The RAG question-and-answer surface. You ask a question in natural language and get a **synthesized answer with citations** back to the source documents, rather than a raw chunk list. This is the layer that turns the indexes into an answer. + +## Mentor + +A mentor chat — the same `roboco_ask_mentor` capability the agents reach through their gateway, exposed to you conversationally for asking about the codebase and accumulated knowledge. + +## Browse + +Browse the indexed content **by category** rather than by query, when you want to see what's in an index instead of searching for something specific. + +## Admin + +The control surface for the indexes themselves. The KB is split into several index types — Documentation, Conversations, Agent Journals, Error Solutions, Standards, Decisions, Code Reviews, and Learnings — and the Admin tab shows **per-index stats** and lets you maintain them: + +| Action | Effect | +|--------|--------| +| **Reindex all** | rebuild every index from source (force) | +| **Refresh** *(per index)* | re-pull and re-embed that one index | +| **Delete** *(per index)* | clear that index's contents | + +!!! warning "Delete and reindex are operational actions" + Clearing an index removes its embeddings, and a full reindex re-embeds everything from scratch — both take real work and real Ollama time. Delete is guarded behind a confirmation dialog. Treat the Admin tab as a maintenance surface, not a daily one. + +## It depends on Ollama + +The whole page — search, ask, mentor, and especially reindexing — runs on the embedding model served by **Ollama** (`qwen3-embedding:0.6b`). If Ollama isn't healthy, queries return nothing useful and reindexing can't embed. + +!!! info "If results come back empty" + A common first-run symptom is an empty or failing KB while Ollama is still pulling models, or when the embedding endpoint is unreachable. Check that the `ollama` service is up and the model is pulled — see [common issues](../troubleshooting/common-issues.md). The RAG engine and its configuration are covered under [deployment](../deploy/deployment.md). + +## Next + +→ [Communications & journals](./communications-and-journals.md) — the raw conversations and journals that feed several of these indexes — or [the Auditor](./auditor.md) for quality oversight. diff --git a/docs/panel/metrics.md b/docs/panel/metrics.md new file mode 100644 index 00000000..a8a2e9f7 --- /dev/null +++ b/docs/panel/metrics.md @@ -0,0 +1,37 @@ +# Metrics + +The Metrics page (`/metrics`) is where you watch the company's throughput and its spend. Two tabs: **Performance** and **Token Usage** (the active tab is in the URL as `?tab=`). + +## Performance + +The Performance tab is a snapshot of velocity and pipeline health, computed from the live task list and the orchestrator's agent status. + +- **Velocity** — completed today, completed this rolling 7 days, total completed all-time, and a completion rate across all tasks. +- **Task Status** — counts of tasks that are pending, in progress, blocked, awaiting QA, and completed. +- **Agent Status** — how many agents are running, idle, waiting (need input), or in error. +- **Team Health** — one card per cell with a health score and its active / blocked / done breakdown. The score is a simple read on blockers: more blocked tasks pulls a cell's health down. A healthy cell sits near 100%; blockers visibly degrade it, so a cell sliding toward red is the signal to look at its [blockers](./command-center.md). + +## Token Usage + +The Token Usage tab is the cost dashboard, scoped to the last 24 hours unless a panel says otherwise. + +- **Summary cards** — tokens in, tokens out, total tokens, total cost over 24h, the trend versus the prior period, and dollars saved by prompt caching. +- **Time series + model donut** — usage over time, and the split across models. +- **Per-agent and per-team bars** — who and which cell is spending. +- **Monthly projection** — projected monthly cost from a rolling-average daily run rate. +- **Cache efficiency** — cache hit rate and the cost it saved. +- **Sessions table** — the recent agent spawn sessions behind the numbers. + +!!! tip "These panels update live" + The token panels subscribe to a live usage stream over `/ws/system` and update in place as agents spend, falling back to periodic HTTP polling when the socket is down. You don't need to refresh to watch cost accrue. + +### Where the dollar figures come from + +Cost is derived from per-session token counts using provider-aware pricing — and local / Ollama usage is intentionally priced at **$0**, so a self-hosted or Ollama-routed workforce shows tokens but no dollars. The full cost model, the budget cap, and where each number originates are documented in [Cost & usage](../operations/cost-and-usage.md); this page only shows the numbers. + +!!! note "Spend against budget lives on the scorecard" + Metrics shows raw usage and projection. Your **monthly budget cap** and whether you're over it appear on the Company Scorecard in [Business](./business.md), not here. + +## Next + +→ [Cost & usage](../operations/cost-and-usage.md) for the pricing model and budget cap · [Health & metrics](../operations/health-and-metrics.md) for operational monitoring · [Command Center](./command-center.md) for the at-a-glance view. diff --git a/docs/panel/projects-and-products.md b/docs/panel/projects-and-products.md new file mode 100644 index 00000000..6dfb8ad3 --- /dev/null +++ b/docs/panel/projects-and-products.md @@ -0,0 +1,45 @@ +# Projects & Products + +Two pages define *what* the company builds and *who* builds it. **Projects** (`/projects`) are the git repositories RoboCo is allowed to touch. **Products** (`/products`) group several repositories into one shipping unit and route each delivery cell to the repo it owns. You only need Products when a single thing you ship spans more than one cell. + +## Projects + +A project is one git repository plus the configuration that tells the company how to build and check it. The Projects page is where you register, search, and edit them. + +- **New** opens the create dialog — name, slug, git URL, GitHub token, assigned cell, default branch, and optional per-project gate commands. +- The list supports **search**, a **cell filter**, and a **show-inactive** toggle so retired repos stay out of the way without being deleted. +- **Edit** reopens the same form to rotate the token, change the gate commands, or flip the assigned cell. The edit dialog also hosts the per-project **Conventions** tab — see [Architectural conventions](../optional/conventions.md). + +The field-by-field detail — what each field means, the token scopes you need, the encryption guarantee, and the default-branch gotcha — lives in [Register your first project](../get-started/first-project.md). Read that page before you create a repo; this page doesn't repeat it. + +!!! tip "Set the gate commands" + The single biggest lever on output quality is pointing a project's `quality_command` and per-step commands at the *real* checks you'd run locally. The company then gates itself the way you would. Details in [Gate commands](../get-started/first-project.md#gate-commands). + +## Products + +A **product** maps each delivery cell to the project (repository) it builds for that product. Use it when one deliverable spans multiple cells — a Backend repo and a Frontend repo, say, that ship together. + +The page is a straightforward table plus a **New** button. A product carries a name and a set of cell-to-project assignments: Backend → repo A, Frontend → repo B, UX/UI → repo C. A cell with no assignment simply doesn't participate in that product. + +### Why the mapping matters + +When the Main PM decomposes a product into work and fans it out to the cells, the branch and PR structure follows the mapping. **The Main PM cuts one integration branch per distinct repository in the product.** A cell's work lands on its repo's integration branch; the cell PM opens the cell→root PR within that repo, and the Main PM opens the root→master PR per repo. Two cells assigned the *same* repo share one integration branch; two cells on *different* repos get one each. + +```mermaid +flowchart LR + P[Product] --> B[Backend cell → repo A] + P --> F[Frontend cell → repo B] + A1[repo A: integration branch] --> MA[root→master PR · repo A] + B1[repo B: integration branch] --> MB[root→master PR · repo B] + B --> A1 + F --> B1 +``` + +!!! note "Single-repo work needs no product" + If everything you're shipping lives in one repo, you don't need a product at all — register the project and hand the Task Assistant or the Create Task dialog that project directly. Products exist purely to coordinate a multi-repo, multi-cell deliverable. + +For how branches assemble and merge once the cells are building, see the [merge model](../company/merge-model.md). For the lifecycle a single task moves through, see the [task lifecycle](../company/task-lifecycle.md). + +## Next + +→ [Tasks & Kanban](./tasks-and-kanban.md) to author and track the work · [Git](./git.md) to watch the branches and PRs land. diff --git a/docs/panel/settings.md b/docs/panel/settings.md new file mode 100644 index 00000000..8f51fbb9 --- /dev/null +++ b/docs/panel/settings.md @@ -0,0 +1,47 @@ +# Settings + +The Settings page (`/settings`) holds two things that genuinely change how the company runs — the **Feature Flags** card and **Transcript Retention** — alongside a handful of cosmetic preferences and read-only connection info. Read the warning below before you trust a switch on this page. + +## Feature Flags + +The Feature Flags card is the operator's master switchboard for the optional, default-off subsystems. Instead of hand-editing environment variables, you flip a switch here. Each row shows the subsystem's label, a one-line description of what it gates, and a toggle. + +| Flag | Gates | +|------|-------| +| External PR review | Discovering and reviewing inbound external/fork PRs. | +| Internal PR review | The read-only safety reviewer on internal branch PRs. | +| Web research | Letting the Board and PMs run web research. | +| Strategy engine | Generating and maintaining strategy artifacts (drives the Command Center's Strategy Signals). | +| Self-healing | Watching RoboCo's own CI and notifying you on a regression. | +| Self-heal originate | Also opening a *pending* fix task for a regression — needs self-healing on, and the task waits for your approval. | +| Pitch provisioning | Auto-provisioning projects from approved [pitches](./business.md). | +| Toolchain match | Provisioning each agent workspace with the target project's own Python and blocking gates when its tests can't run. | +| Conventions | Enforcing the per-project architectural standard (`.roboco/conventions.yml`). | +| RAG auto-update | Keeping the [knowledge base](./knowledge-base.md) index refreshed automatically. | +| Transcript prune | Running the background sweep that prunes old transcripts. | + +Each subsystem has a full page in the [optional subsystems section](../optional/index.md) — what it does, the exact `ROBOCO_*` env var behind it, and what turning it on changes. + +!!! warning "Flags take effect on the next backend restart" + Toggling a flag persists the choice server-side, but it does **not** hot-reload — the backend reads it at startup. The toast says as much: "takes effect on next restart." A flag you've never set falls back to its environment / config default. So: flip it here, then restart the orchestrator for it to take hold. + +## Transcript Retention + +The Transcript Retention card sets how many days agent transcripts are kept before the prune sweep removes them (default 14). This is a real, server-persisted setting. It only ever prunes *agent-owned* transcripts — never your own Claude sessions. The sweep itself is gated by the **Transcript prune** feature flag above. + +## Connection Info (read-only) + +This card displays the API and WebSocket base URLs the panel is using (`NEXT_PUBLIC_API_URL`, `NEXT_PUBLIC_WS_URL`) — relative by default so nginx dispatches them on a single origin. It's informational; you can't edit it here. + +## The cosmetic and mock-state controls + +!!! danger "Some switches on this page do nothing" + The page also carries **Appearance** (theme, collapsed sidebar — these work and persist locally), a **User Info** card, and a **Data & Refresh** + **Notifications** block. The Auto Refresh, Refresh Interval, Enable Notifications, and Sound Alerts controls — and the page-level **Save Settings** button — are **local component state only.** They are *not* persisted or wired to anything; Save just shows a success toast. Don't rely on them to change runtime behavior. Only **Feature Flags**, **Transcript Retention**, and the **Appearance** preferences actually do something. + +## AI provider configuration lives elsewhere + +Choosing which model and provider backs each agent is **not** on this page — it's the **AI Providers** page (`/settings/ai-providers`), linked from the sidebar footer. There you set the global routing mode (Anthropic / Grok / Ollama / self-hosted), or pin individual agents in Mix mode, and store the encrypted provider keys. See [Provider routing](../models/provider-routing.md). + +## Next + +→ [Optional subsystems](../optional/index.md) for every feature flag in detail · [Provider routing](../models/provider-routing.md) for the AI Providers page · [Environment reference](../deploy/env-reference.md) for the `ROBOCO_*` variables behind the flags. diff --git a/docs/panel/tasks-and-kanban.md b/docs/panel/tasks-and-kanban.md new file mode 100644 index 00000000..37b942c4 --- /dev/null +++ b/docs/panel/tasks-and-kanban.md @@ -0,0 +1,59 @@ +# Tasks & Kanban + +Tasks are the unit of work in RoboCo — nothing happens without one. The panel gives you three views of them: the **Tasks table** (`/tasks`) for searching and filtering everything, the **Task Detail** page (`/tasks/[id]`) for the full state of one task plus your god-mode overrides, and the **Dev Kanban** (`/kanban`) for a pipeline-style board of where work is in flight. + +## The Tasks table (`/tasks`) + +The master list of every task. It gives you full-text search and multi-select filters across **status, team, task type, project, and product**, with sortable, paginated, expandable rows. All of that filter/sort/page state lives in the URL query string, so a filtered view is shareable and survives the back button. + +The **Create Task** dialog lets you author a task by hand — title, acceptance criteria, project/product, type. If you'd rather describe a rough idea and have an agent read your code and draft a properly-formed task with acceptance criteria, use the conversational [Task Assistant](../get-started/first-task.md) at `/prompter` instead. + +## Task Detail (`/tasks/[id]`) + +The heaviest operator surface in the panel. The header carries the task's metadata; the body is tabbed (notes, commits, acceptance criteria, and more). + +### Clickable branch and PR + +When a task has a branch or a pull request, the git badges are **live links** into your repository's web host — the branch jumps to `…/tree/<branch>` and the PR to `…/pull/<n>`, derived from the project's git URL. If the URL can't be parsed into a web link, it falls back to a plain label rather than a broken link, so you always see *something* useful. + +### Per-role note sections + +The **Notes** tab carries a separate, editable section for each role that touches a task, so the handoff trail is structured rather than a single free-text blob: + +| Section | Written by | +|---------|-----------| +| **Developer Notes** | the dev (`dev_notes`) | +| **Documenter Notes** | the documenter (`doc_notes`) | +| **QA Notes** | QA (`qa_notes`) — with a pass/fail verdict pill from `qa_verified` | +| **PR Reviewer Notes** | the PR reviewer (`pr_reviewer_notes`) | +| **Auditor Notes** | the Auditor (`auditor_notes`) | + +Empty sections still render so you can see what's expected — and add a note yourself if you need to. + +### CEO god-mode { #ceo-god-mode } + +The lifecycle is normally enforced role-by-role at the [gateway](../company/agent-gateway.md), but *you* are the CEO and can override any of it from this page. The action set covers the whole lifecycle: claim, start, pause, resume, block, unblock, verify, submit-qa, pass-qa / fail-qa, docs-complete, submit-pm-review, approve-and-merge, escalate-to-ceo, ceo-approve / ceo-reject, and cancel. You can also run git directly — **create a branch, create a PR, merge a PR** — and those operations are performed as agent id **`ceo`**. + +This page also exposes **Approve & Start** for a board-reviewed pending task and **Re-draft with board feedback**, which routes back to the Task Assistant (`/prompter?redraft=`). + +!!! warning "Every override forces an audit note" + The state-changing actions — pass/fail QA, approvals, rejections, cancellation — open a dialog that requires you to type a note before they go through. That note is written permanently to the task's history. God-mode is powerful by design; it is never silent. + +See [the task lifecycle](../company/task-lifecycle.md) for what each transition means and [the merge model](../company/merge-model.md) for how PRs flow up to master. + +## Dev Kanban (`/kanban`) + +A swim-lane board of the delivery pipeline, switched with the `?view=` query param into four boards: + +| View | `?view=` | Shows | +|------|----------|-------| +| **Developer** | `dev` | tasks in the development states | +| **QA** | `qa` | tasks awaiting / in QA review | +| **PR Review** | `pr-review` | assembled PRs at the in-path review gate | +| **PM** | `pm` | tasks awaiting PM review and merge | + +Each board is a read-at-a-glance view of where work sits in the [lifecycle](../company/task-lifecycle.md). Switching tabs updates the URL, so a specific board is shareable. + +## Next + +→ [Agents & work sessions](./agents-and-work-sessions.md) to see who's working these tasks, or [Git](./git.md) to inspect the branches and PRs they produce. diff --git a/docs/troubleshooting/common-issues.md b/docs/troubleshooting/common-issues.md new file mode 100644 index 00000000..464120bb --- /dev/null +++ b/docs/troubleshooting/common-issues.md @@ -0,0 +1,78 @@ +# Common issues + +When something goes wrong, the failure is almost always one of a handful of things: a mount that didn't make it into the container, a model that isn't pulled, a token that expired, or a provider that's parked rather than hung. This page is the problem→cause→fix guide for the snags you'll actually hit, ordered by how often they bite on a fresh deployment. + +## Quick reference + +| Symptom | Most likely cause | Fix | +|---------|-------------------|-----| +| Agents spawn but do nothing useful (tool-discovery churn) | The role's tool-manifest didn't mount; the agent falls back to discovering verbs | Check the manifest mount (below) | +| Agent containers respawn in a loop, MCP servers stuck "pending" | MCP server launched without `--no-sync` against a workspace clone | Already fixed in the orchestrator; verify your image is current (below) | +| A provider's agents go quiet all at once | The provider is **parked-and-probing** after a 429/overload — not hung | Wait; it self-resumes. See [Resilience](../models/resilience.md) | +| KB / `ask_mentor` returns nothing | Ollama unhealthy or models not pulled | Check `ollama-init` logs (below) | +| Agent containers exit immediately | `~/.claude` not mounted, or a Grok token expired | Check the mount / refresh the token (below) | +| Clone fails, agent can't reach the repo | Missing or invalid project PAT, or HTTPS URL with no token | Set the project token (below) | +| Orchestrator won't start | `ROBOCO_ENCRYPTION_KEY` unset, or a pending migration | First-run checklist (below) | + +## Agents spawn but do nothing + +If agents come up but burn turns "looking for tools" instead of claiming work, the cause is almost always the **tool manifest**. At spawn the orchestrator writes a per-agent manifest and mounts it read-only at `/app/tool-manifest.json` (the mount is built in `roboco/runtime/orchestrator.py`, pointing `ROBOCO_TOOL_MANIFEST_PATH` at that path). The manifest lists exactly the verbs the agent's role may call — see [the gateway](../company/agent-gateway.md). + +If that file is missing inside the container, the agent has no allow-list to read and falls back to discovering the verb surface itself, which wastes turns and tokens. The mount source is `{DATA_HOST_PATH}/manifests/{agent_id}.json` on the host. Confirm the host directory exists and is writable, and that the data volume is mounted into the orchestrator so it can write manifests there in the first place. + +!!! tip "Verify the mount inside the container" + Exec into a running agent container and check that `/app/tool-manifest.json` exists and is non-empty. If it's absent, the orchestrator couldn't write or mount the per-agent manifest — fix the host `manifests/` directory and the orchestrator's data-volume mount, then respawn. + +## Respawn loop with MCP servers stuck "pending" + +A historical failure mode: each agent's MCP servers (`roboco-flow`, `roboco-do`, the read-only git and KB servers) launch via `uv run` from inside the agent's workspace clone. If `uv` re-syncs that clone on launch it can collide with the clone's lock, the gateway servers never come up, the agent has zero verbs, and the orchestrator respawns it in a loop. + +The fix is already in the code: every MCP server is launched with `uv run --no-sync` (`roboco/runtime/orchestrator.py`), and the SDK startup hook pins `uv` to the pre-baked image venv. If you see this symptom, your running image predates the fix — rebuild and redeploy the agent images so the `--no-sync` launch is in effect. + +## A "quiet" provider is parked, not hung + +If every agent on one provider goes silent at the same moment, it is almost never a crash. On a provider 429, or a persistent overload (HTTP 529/500/503), RoboCo **parks** that provider's work and runs a background probe that resumes it the moment the provider recovers — it does not crash-retry into the wall and burn tokens. You'll see an amber banner in the panel; the work revives on its own. + +!!! note "Don't restart to 'unstick' it" + Restarting the orchestrator throws away the park-and-probe state and the parked agents' context. Leave it alone — it self-heals. The full mechanism, the banner, and the `ROBOCO_OVERLOAD_BREAK_ENABLED` flag are documented in [Resilience](../models/resilience.md). + +## KB search / ask-mentor returns nothing + +The knowledge base and `roboco_ask_mentor` run on the in-house pgvector RAG, which depends on Ollama for embeddings and the local LLM. If the KB is empty or `ask` comes back blank, Ollama is the usual culprit: + +- **Models not pulled.** `ollama-init` pulls `qwen3-embedding:0.6b` (the embedder, ~30s) and the local LLM (~2min) on startup. Check `docker logs roboco-ollama-init` — if the pull failed or is still running, embeddings can't be produced and indexing/retrieval returns nothing. +- **Ollama unhealthy.** The healthcheck is `ollama list`, not a curl. If the `ollama` service is unhealthy the orchestrator's document indexing (run during FastAPI lifespan) never completes. +- **A `404 /api/embed`** in the logs means the embedding model isn't present — re-check `ollama-init`. + +See [Health and metrics](../operations/health-and-metrics.md) for where these surface in the panel. + +## Agent containers exit immediately + +A container that starts and dies within seconds is missing something it needs at boot: + +- **`~/.claude` not mounted (Claude agents).** Claude-backed agents mount the host's `~/.claude` (overridable via `ROBOCO_HOST_CLAUDE_DIR`) into the container so they can authenticate. If that directory is absent or unreadable on the host, the agent can't start. Confirm the host directory exists and the orchestrator user can read it. +- **Grok token expired (Grok agents).** Grok's access token has a fixed ~6h TTL and the CLI can't refresh it headlessly — on an expired token it would otherwise hang forever at an interactive login prompt. The entrypoint runs `python -m roboco.llm.providers.grok_auth --check` as a preflight and **exits 78** immediately rather than hang (`docker/scripts/grok-cli-agent-entrypoint.sh`). An exit code of 78 means the Grok token is missing or expired. The orchestrator mints a fresh token before expiry on each dispatch tick; if you keep hitting 78, re-run `grok login` on the host to repopulate the shared `auth.json`. + +## Clone fails / agent can't reach the repo + +Cloning is the first thing an agent does on a project, and it fails for one reason in practice: the **project's GitHub token**. Git auth is per-project, not global. + +- An **HTTPS Git URL with no token fails** — the agent can't authenticate. Set the token when you create the project. +- An **invalid or under-scoped token** fails the same way. The token needs repository contents access (clone/push) and pull-request access. See [Register a project](../get-started/first-project.md#the-github-token) for the exact scopes. + +Remember the API never returns a stored token, so the panel only shows whether one is set, not its value — if a clone fails, re-enter the token rather than assuming the stored one is good. + +!!! warning "If you rotated ROBOCO_ENCRYPTION_KEY" + Every project PAT is Fernet-encrypted with `ROBOCO_ENCRYPTION_KEY`. Change that key and all stored tokens become undecryptable — clones will fail until you re-enter every token. See [Security](./security.md). + +## First-run and migration checks + +When the orchestrator won't come up at all on a fresh deploy: + +- **`ROBOCO_ENCRYPTION_KEY` must be set.** It defaults to empty in config, but the orchestrator refuses to start without it. Generate one with `python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())'` and set it in your `.env`. It is load-bearing and must never change after first use. +- **Migrations.** Schema changes ship as Alembic migrations under `alembic/versions/`. The API applies them on startup (and falls back to `create_all` on a fresh DB), but after pulling a change that adds a migration you can apply it explicitly with `docker compose exec orchestrator alembic upgrade head`. +- **Startup is slow on purpose.** The FastAPI lifespan does ~30–60s of document indexing before the API answers, and the orchestrator polls `/health` for up to 120s before starting its dispatch loop. An "All connection attempts failed" early in the logs usually just means a dependent service hadn't finished its healthcheck yet — give the startup sequence time before treating it as an error. + +## Next + +→ [Security](./security.md) — the trust model and how to harden a deployment, or back to the [troubleshooting index](./index.md). diff --git a/docs/troubleshooting/index.md b/docs/troubleshooting/index.md new file mode 100644 index 00000000..6ec17236 --- /dev/null +++ b/docs/troubleshooting/index.md @@ -0,0 +1,19 @@ +# Troubleshooting & Security + +When something looks stuck, or before you put RoboCo anywhere near a network you don't fully trust, start here. + +<div class="grid cards" markdown> + +- **[Common issues](common-issues.md)** + + --- + + The usual snags — agents that spawn but do nothing, a "quiet" provider that's actually parked, an empty knowledge base — with their causes and fixes. + +- **[Security](security.md)** + + --- + + The trust model: header-trust versus secure mode, how secrets and tokens are handled, and the one rule — don't expose it to the public internet. + +</div> diff --git a/docs/troubleshooting/security.md b/docs/troubleshooting/security.md new file mode 100644 index 00000000..8a4ca5eb --- /dev/null +++ b/docs/troubleshooting/security.md @@ -0,0 +1,54 @@ +# Security + +RoboCo runs a workforce of agents with access to your repositories, so its security model is worth understanding before you put it anywhere. The short version: it is built for a **trusted private network**, it ships in a permissive header-trust mode by default, and you harden it with one flag and one token. This page covers the trust model, how to lock it down, where secrets live, and the one rule that matters more than the rest. + +!!! danger "Do not expose RoboCo to the public internet" + RoboCo is designed to run on a trusted LAN behind nginx, which is the only externally-exposed service. The default authentication mode trusts request headers (below), and one WebSocket stream is unauthenticated by design. Treat the whole system as you would a database: reachable only from your own network, never from the open internet. + +## The default is header-trust mode + +By default the API authenticates an agent purely from request headers: `X-Agent-ID`, `X-Agent-Role`, and optionally `X-Agent-Team`. There is no signed token required, which means **any client that can reach the API can claim any role — including `ceo`.** This is intentional for a single-operator deployment on a trusted network (it keeps the panel and `curl`-for-debugging simple), and the API logs a loud warning at startup whenever it's in this mode (`roboco/api/app.py`): + +> Agent auth is in HEADER-TRUST mode … the API accepts X-Agent-Id / X-Agent-Role without verifying a signed token, so any client that can reach it may act as any role, including 'ceo'. Acceptable only on a trusted private network. + +If your network boundary is solid, header-trust is fine. If it isn't — or you just want defense in depth — turn on secure mode. + +## Hardening: secure mode + the panel token + +Set `ROBOCO_AGENT_AUTH_REQUIRED=true` to require a signed token on every request. In this mode: + +- Every request must carry an `X-Agent-Token` that is an HMAC of `agent_id:role:team` signed with `ROBOCO_AGENT_AUTH_SECRET`. The orchestrator mints this token for each agent at spawn, so agent traffic keeps working transparently. +- A missing token is rejected with `401`. A **forged role won't help**: even in header-trust mode, any token that *is* presented is still verified, so you can't bypass auth by supplying an invalid token. The HMAC comparison is constant-time. +- The **panel** authenticates as the CEO, so it needs a CEO token to keep working in secure mode. Mint it with `make panel-token`, which prints the signed token (and refuses with an error if `ROBOCO_AGENT_AUTH_SECRET` is unset, since an unsigned token would be useless). Configure the panel with that token. + +!!! tip "Set the secret before you flip the flag" + `ROBOCO_AGENT_AUTH_SECRET` is the signing key for every agent and panel token. Set it first, then `make panel-token`, then set `ROBOCO_AGENT_AUTH_REQUIRED=true` — otherwise agents and the panel can't produce valid tokens and every request 401s. If the secret is unset, token minting fails closed to a literal `UNSIGNED` value rather than producing a usable token. + +See [API authentication](../api/auth.md) for the exact headers and how the panel and external clients pass them. + +## Per-agent token binding + +The token isn't a single shared bearer secret — it's **bound to the specific agent**. Because the HMAC covers `agent_id:role:team`, a token issued for `be-dev-1` as a `developer` is only valid for those exact header values. An agent on the Docker network can't take its own valid token and replay it claiming to be `main_pm` or `ceo`; the signature won't match the forged role, and the request is rejected. This is what stops one agent from escalating its own privileges by editing headers. + +## Secrets handling + +Two secrets are load-bearing, and both stay out of the repo: + +| Secret | What it protects | Rule | +|--------|------------------|------| +| `ROBOCO_ENCRYPTION_KEY` | Fernet key that encrypts every per-project GitHub PAT at rest | Set once at install; **never change it** or all stored tokens become undecryptable. Back it up. | +| `ROBOCO_AGENT_AUTH_SECRET` | HMAC signing key for agent and panel tokens | Required for secure mode; keep it off the repo and out of logs. | + +Project GitHub tokens are **encrypted the moment you save them** (with `ROBOCO_ENCRYPTION_KEY`) and stored in the `projects.git_token_encrypted` column. The **API never returns a token** — responses only carry a `has_git_token` boolean, so the panel can show whether a token is set but never its value. Neither secret belongs in the repository; put them in your `.env` (which is gitignored) or your secret manager. + +## The PAT never enters an agent container + +This is the guarantee that makes it safe to hand RoboCo a private repo: **your GitHub PAT is never present inside an agent container.** The orchestrator decrypts the token only at the moment of a git operation, injects it for that operation, and immediately after cloning **scrubs the token out of the clone's git config** — then verifies no token byte survives anywhere under `.git/`, destroying the workspace if one did. A compromised or misbehaving agent has nothing to exfiltrate, because the credential was never on its disk. The clone scrub is described in [Register a project](../get-started/first-project.md#what-happens-under-the-hood), and the broader sandboxing model in [the gateway](../company/agent-gateway.md). + +## WebSocket auth caveat + +The per-resource WebSocket streams are keyed to a resource, but the operator stream is not authenticated. **`/ws/system` carries no per-agent keying and no token** even when `ROBOCO_AGENT_AUTH_REQUIRED=true` — secure mode does not extend to it. It is **read-only** (it carries system events like rate-limit lifecycle and usage snapshots; it accepts nothing from the client), so the exposure is limited to a reader seeing system telemetry. It is, however, one more reason the system must sit on a trusted network: anyone who can open that socket can watch the operator stream. + +## Next + +→ [API authentication](../api/auth.md) for the header/token contract, [the gateway](../company/agent-gateway.md) for how agent capability is constrained, or back to [common issues](./common-issues.md). diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..ef8f60c7 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,182 @@ +# RoboCo documentation site. +# Build: uv run mkdocs build --strict +# Preview: uv run mkdocs serve +# Deploy: handled in CI by .github/workflows/docs.yml (mkdocs gh-deploy → gh-pages). +site_name: RoboCo +site_url: https://roboco.dev/docs/ +site_description: >- + RoboCo is a self-hosted AI agentic company — 25 AI agents and one human CEO + that run as a complete software-development workforce, operated from a single + control panel. +site_author: Renzo Franceschini +copyright: Copyright © 2026 Renzo Franceschini — licensed under AGPL-3.0 + +repo_url: https://github.com/rennf93/roboco +repo_name: rennf93/roboco +edit_uri: edit/master/docs/ + +docs_dir: docs + +# Everything under docs/ that is NOT user-facing: the agent-facing RAG corpus, +# the gitignored scratch trees, and the orphaned per-area stubs. +exclude_docs: | + rag/ + internal/ + superpowers/ + backend/ + frontend/ + ux_ui/ + features/ + bugs/ + initiatives/ + self/ + models/README.md + +theme: + name: material + logo: assets/logo.png + favicon: assets/logo.png + icon: + repo: fontawesome/brands/github + palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: black + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to system preference + features: + - navigation.instant + - navigation.instant.progress + - navigation.tracking + - navigation.tabs + - navigation.tabs.sticky + - navigation.sections + - navigation.indexes + - navigation.top + - navigation.footer + - toc.follow + - search.suggest + - search.highlight + - content.code.copy + - content.code.annotate + - content.tabs.link + +markdown_extensions: + - abbr + - admonition + - attr_list + - def_list + - footnotes + - md_in_html + - tables + - toc: + permalink: true + - pymdownx.details + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +plugins: + - search + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/rennf93/roboco + name: RoboCo on GitHub + - icon: fontawesome/brands/youtube + link: https://www.youtube.com/watch?v=t1QNqJgBmkM + name: The RoboCo intro on YouTube + +nav: + - Home: index.md + - Get Started: + - get-started/index.md + - Install & first run: get-started/installation.md + - Register your first project: get-started/first-project.md + - Your first task: get-started/first-task.md + - The Company: + - company/index.md + - Org & roles: company/org-and-roles.md + - The task lifecycle: company/task-lifecycle.md + - The merge model: company/merge-model.md + - How agents are sandboxed: company/agent-gateway.md + - The Tour: + - how-to/README.md + - The shape of the company: how-to/01-the-company.md + - It starts with you: how-to/02-it-starts-with-you.md + - The cells build it: how-to/03-the-cells-build-it.md + - The last call — and the loop: how-to/04-the-last-call-and-the-loop.md + - The business workflow: how-to/05-the-business-workflow.md + - Operating the Panel: + - panel/index.md + - Command Center: panel/command-center.md + - Tasks & Kanban: panel/tasks-and-kanban.md + - Agents & Work Sessions: panel/agents-and-work-sessions.md + - Git: panel/git.md + - Knowledge Base: panel/knowledge-base.md + - Projects & Products: panel/projects-and-products.md + - Communications & Journals: panel/communications-and-journals.md + - Auditor: panel/auditor.md + - Business: panel/business.md + - Metrics: panel/metrics.md + - Settings: panel/settings.md + - Choosing & Running Models: + - models/index.md + - Provider routing: models/provider-routing.md + - Running on Grok: models/grok.md + - Resilience: models/resilience.md + - Cost & Observability: + - operations/index.md + - Cost & usage: operations/cost-and-usage.md + - Health & metrics: operations/health-and-metrics.md + - Optional Subsystems: + - optional/index.md + - Architectural Conventions: optional/conventions.md + - Toolchain matching: optional/toolchain-matching.md + - Web research: optional/web-research.md + - Strategy engine: optional/strategy-engine.md + - Pitch provisioning: optional/pitch-provisioning.md + - External / internal PR review: optional/pr-review.md + - Self-healing CI: optional/self-heal.md + - Configure & Deploy: + - deploy/index.md + - Deployment: deploy/deployment.md + - Environment reference: deploy/env-reference.md + - Data & migrations: deploy/data-and-migrations.md + - Bootstrap & seeds: deploy/bootstrap-and-seeds.md + - API Reference: + - api/index.md + - REST API: api/rest-api.md + - WebSocket streams: api/websockets.md + - Authentication: api/auth.md + - Troubleshooting & Security: + - troubleshooting/index.md + - Common issues: troubleshooting/common-issues.md + - Security: troubleshooting/security.md diff --git a/pyproject.toml b/pyproject.toml index 659654a2..96c0e2ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,10 +88,11 @@ docs = [ "mkdocs", "mkdocs-material", "mkdocstrings[python]", + "pymarkdownlnt", ] [project.scripts] -roboco = "roboco.cli:main" +roboco = "roboco.cli:cli" roboco-bootstrap = "roboco.bootstrap:cli" [project.urls] @@ -410,6 +411,7 @@ DEP002 = [ "mkdocs", "mkdocs-material", "mkdocstrings", + "pymarkdownlnt", ] # DEP003: Starlette is a transitive dep of FastAPI, but BaseHTTPMiddleware is needed DEP003 = ["starlette"] diff --git a/usage.md b/usage.md index da8ac3dc..6d0d1972 100644 --- a/usage.md +++ b/usage.md @@ -1,264 +1,13 @@ # RoboCo Usage Guide -Operating the AI company after deployment. +> **This guide has moved.** Operating RoboCo is now documented in the full docs site at **[roboco.dev/docs](https://roboco.dev/docs)** (source under [`docs/`](docs/)). -## The Organization +Jump straight to: -22 AI agents organized as a company: +- **[Your first task](docs/get-started/first-task.md)** — hand the company work and watch it run. +- **[Operating the Panel](docs/panel/index.md)** — a page-by-page reference for the control panel. +- **[The task lifecycle](docs/company/task-lifecycle.md)** — the states every task moves through. +- **[Cost & observability](docs/operations/cost-and-usage.md)** — token usage, cost, and resource footprint. +- **[API reference](docs/api/rest-api.md)** — creating and monitoring tasks over the API. -``` -CEO (You) -├── Intake (on-demand interviewer: chats only with you to draft a task) -├── Secretary (your chief-of-staff: runs gated directives under your command) -└── Board - ├── Product Owner - ├── Head of Marketing - ├── PR Reviewer (read-only: gates inbound external/fork PRs) - └── Auditor - └── Main PM - ├── Backend Cell (PM, 2 Devs, QA, Documenter) - ├── Frontend Cell (PM, 2 Devs, QA, Documenter) - └── UX/UI Cell (PM, 2 Devs, QA, Documenter) -``` - -## Agent IDs - -| ID | Role | Team | -|----|------|------| -| `main-pm` | Main PM | Management | -| `be-pm` | Cell PM | Backend | -| `be-dev-1`, `be-dev-2` | Developers | Backend | -| `be-qa` | QA | Backend | -| `be-doc` | Documenter | Backend | -| `fe-pm` | Cell PM | Frontend | -| `fe-dev-1`, `fe-dev-2` | Developers | Frontend | -| `fe-qa` | QA | Frontend | -| `fe-doc` | Documenter | Frontend | -| `ux-pm` | Cell PM | UX/UI | -| `ux-dev-1`, `ux-dev-2` | Developers | UX/UI | -| `ux-qa` | QA | UX/UI | -| `ux-doc` | Documenter | UX/UI | -| `product-owner` | Product Owner | Board | -| `head-marketing` | Head of Marketing | Board | -| `auditor` | Auditor | Board | -| `intake-1` | Intake (interviewer) | Board | -| `secretary-1` | Secretary (chief-of-staff) | Board | -| `pr-reviewer-1` | PR Reviewer (read-only) | Board | - -## Spawning Agents - -```bash -# Start with minimal team -uv run python -m roboco.cli --spawn main-pm be-dev-1 be-qa - -# Add more agents -uv run python -m roboco.cli --spawn main-pm be-pm be-dev-1 be-dev-2 be-qa - -# Full organization -uv run python -m roboco.cli --spawn \ - main-pm \ - be-pm be-dev-1 be-dev-2 be-qa be-doc \ - fe-pm fe-dev-1 fe-dev-2 fe-qa fe-doc \ - ux-pm ux-dev-1 ux-dev-2 ux-qa ux-doc \ - product-owner head-marketing auditor -``` - -## Monitoring Agents - -### Check Status - -```bash -# Via API -curl http://localhost:8000/api/orchestrator/status | jq - -# Via Docker -docker ps --filter "name=roboco-agent" -``` - -### View Agent Logs - -```bash -# Follow specific agent's output -docker logs -f roboco-agent-be-dev-1 - -# All agent containers -docker ps --filter "name=roboco-agent" --format "{{.Names}}" -``` - -### Container Management - -```bash -# Stop one agent -docker stop roboco-agent-be-dev-1 - -# Restart an agent -docker restart roboco-agent-be-dev-1 - -# Stop all agents -docker ps --filter "name=roboco-agent" -q | xargs docker stop -``` - -## Creating Tasks - -`POST /api/tasks` has no silent defaults — `title`, `description` (min 20 chars), `acceptance_criteria` (at least one), `team`, `task_type`, `nature`, and `estimated_complexity` are all required, plus exactly one of `project_id` (the repo this task targets) or `product_id` (a cell→project map for a fan-out task). See the `TaskCreate` schema in `roboco/models/task.py` (or the Swagger UI at [docs](./docs)) for the full field list and enum values. - -```bash -curl -X POST http://localhost:8000/api/tasks \ - -H "Content-Type: application/json" \ - -d '{ - "title": "Implement user authentication", - "description": "Add JWT-based auth to the API endpoints", - "team": "backend", - "task_type": "code", - "nature": "technical", - "estimated_complexity": "medium", - "project_id": "<project-uuid>", - "acceptance_criteria": [ - "Users can register", - "Users can login", - "Protected routes require valid JWT" - ] - }' -``` - -Enum values: `task_type` ∈ {`code`, `documentation`, `research`, `planning`, `design`, `administrative`}; `nature` ∈ {`technical`, `non_technical`}; `estimated_complexity` ∈ {`low`, `medium`, `high`}. - -## Task Lifecycle - -``` -pending → claimed → in_progress → verifying → awaiting_qa → awaiting_documentation → awaiting_pm_review → completed - ↓ ↓ - blocked/paused awaiting_ceo_approval (major tasks) - -in_progress → awaiting_pr_review → awaiting_pm_review → completed (in-path PR-review gate for assembled PRs) - ↓ - needs_revision (pr_fail) -``` - -The leaf flow above is the developer path. Assembled PRs add an in-path PR-review gate: the cell PM's `submit_up` opens the cell→root PR and the Main PM's `submit_root` opens the root→master PR, each moving the task `in_progress → awaiting_pr_review`. There the PR reviewer `pr_pass`es it on to `awaiting_pm_review` or `pr_fail`s it back to `needs_revision`. Leaf developer tasks and branchless coordination roots skip the gate. - -Agents automatically: -1. Pull pending work via the gateway verb `give_me_work()` -2. Claim it with `i_will_work_on(task_id)` (auto-creates the feature branch) -3. Follow the workflow: UNDERSTAND → PLAN → EXECUTE → VERIFY → NOTES -4. Open a PR and submit for QA when done (`open_pr` / `i_am_done`) -5. Move to next task - -## API Endpoints - -| Endpoint | Description | -|----------|-------------| -| `GET /health` | Health check | -| `GET /docs` | Swagger UI | -| `GET /api/orchestrator/status` | Agent states | -| `GET /api/tasks` | List tasks | -| `POST /api/tasks` | Create task | -| `GET /api/tasks/{id}` | Task details | - -## Viewing the API - -Open http://localhost:8000/docs in your browser for the Swagger UI. - -## Common Workflows - -### Start a Development Session - -```bash -# 1. Start infrastructure -docker compose up -d - -# 2. Run migrations (if needed) -uv run alembic upgrade head - -# 3. Start with a small team -uv run python -m roboco.cli --spawn main-pm be-dev-1 be-qa -``` - -### Create and Monitor a Task - -```bash -# Create task (all fields below are required — see POST /api/tasks schema) -curl -X POST http://localhost:8000/api/tasks \ - -H "Content-Type: application/json" \ - -d '{ - "title": "Fix login bug", - "description": "Login fails on expired-token refresh path", - "team": "backend", - "task_type": "code", - "nature": "technical", - "estimated_complexity": "low", - "project_id": "<project-uuid>", - "acceptance_criteria": ["Expired token refreshes without 500"] - }' - -# Watch agent pick it up -docker logs -f roboco-agent-be-dev-1 -``` - -### Shutdown - -```bash -# Stop orchestrator (Ctrl+C in terminal) - -# Stop agent containers -docker ps --filter "name=roboco-agent" -q | xargs docker stop - -# Stop infrastructure -docker compose down -``` - -## Tips - -### Start Small - -Don't spawn the whole fleet at once. Start with: -1. `main-pm` alone - verify spawning works -2. Add `be-dev-1` - verify task claiming -3. Add `be-qa` - verify full workflow - -### Check Agent Health - -```bash -# Quick status -curl -s http://localhost:8000/api/orchestrator/status | jq '.agents' - -# Detailed container info -docker inspect roboco-agent-be-dev-1 -``` - -### Debug an Agent - -```bash -# View full logs -docker logs roboco-agent-be-dev-1 - -# Attach to container (read-only) -docker logs -f roboco-agent-be-dev-1 -``` - -### Resource Usage - -RAM is modest. Agent containers are spawned on demand and torn down when their work is done, so you rarely have more than a handful live at once — and the on-demand Intake and Secretary only run while you're interacting with them. Steady-state memory is dominated by the standing services (Postgres, Redis, and especially Ollama with its models loaded), not by the agents. - -Measured at idle on the reference NAS (full stack up, no task running), the standing services use roughly: - -| Service | RAM (idle) | -|---------|------------| -| Ollama (models loaded) | ~2.2 GB | -| Orchestrator | ~150 MB | -| Postgres | ~60 MB | -| Panel | ~35 MB | -| Redis | ~15 MB | -| nginx | ~10 MB | - -So the whole standing stack idles around ~2.5 GB, almost all of it Ollama; the application itself is a few hundred MB. - -Under load it stays light. Measured with five agents working concurrently (two cells' developers plus a cell PM), each agent container used ~0.5–0.65 GB, and the whole stack — agents plus services — peaked around ~6.6 GB, roughly 5% of a 128 GB box. The orchestrator itself grows with concurrency (~150 MB idle → ~1 GB while managing several live agent sessions and their streams), and a developer briefly spikes to a few CPU cores while it is actively generating. Even at full-fleet peak you stay well under ~10 GB — RAM is not the constraint; storage is. - -Storage is the larger footprint: the image set. The agent images all build `FROM` a shared base layer, so on disk they cost far less than their nominal sizes added together. For reference, the panel image is ~230 MB, the orchestrator ~0.9 GB, the agent base ~1.1 GB, and each agent image ~1.1 GB (the frontend dev/QA images are larger, ~1.9 GB, for their browser/Node toolchain) — but the shared base means the real on-disk total is well below their sum. `docker system prune` reclaims old image versions, stopped agent containers, and build cache (typically a few GB). - -Monitor with: -```bash -docker stats # live RAM / CPU per running container -docker system df # image / container / build-cache disk usage -``` +Preview the site locally with `make serve-docs`. diff --git a/uv.lock b/uv.lock index 87fd21d0..69586359 100644 --- a/uv.lock +++ b/uv.lock @@ -74,6 +74,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" }, ] +[[package]] +name = "application-file-scanner" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "py-walk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/22/e872546d298103527380955f51191ff87cd178e6aac62bb87de73c3e074f/application_file_scanner-0.6.4.tar.gz", hash = "sha256:581c48c5017345747be7f49507da84fec36d1f7b4f67003e9fbaf2f0bc6a3f66", size = 28540, upload-time = "2026-01-19T23:32:15.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/83/e9a5a14c1b6c20ad44abcdb7bb2f34d860aa4f4ee64c526247066b6c42fb/application_file_scanner-0.6.4-py3-none-any.whl", hash = "sha256:49c211c60f1932812477facc38701d58037b07b031fdb6a6061fdee3fd53e35f", size = 15445, upload-time = "2026-01-19T23:32:14.4Z" }, +] + +[[package]] +name = "application-properties" +version = "0.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyjson5" }, + { name = "pyyaml" }, + { name = "tomli" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/bb/321da0e373416080801d05b8dd9c0a8da77fe305c697adf8c025470ce170/application_properties-0.9.3.tar.gz", hash = "sha256:7dc7d8f23d11e539427e7b8e3afa70353e159c16f703bad6dd082cc6cdfeeaa8", size = 43304, upload-time = "2026-06-02T03:52:07.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/72/3ac1bda73e9c95444b1e13a0de73e847048a776e5ac5b3977c37f04167f9/application_properties-0.9.3-py3-none-any.whl", hash = "sha256:32dbd62b3fb657ec1c7fdc352590463c2ffd14950ffd258a5c091e8798d8aec0", size = 24595, upload-time = "2026-06-02T03:52:06.416Z" }, +] + [[package]] name = "ast-serialize" version = "0.5.0" @@ -444,6 +472,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "columnar" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "toolz" }, + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/0d/a0b2fd781050d29c9df64ac6df30b5f18b775724b79779f56fc5a8298fe9/Columnar-1.4.1.tar.gz", hash = "sha256:c3cb57273333b2ff9cfaafc86f09307419330c97faa88dcfe23df05e6fbb9c72", size = 11386, upload-time = "2021-12-27T21:58:56.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/00/a17a5657bf090b9dffdb310ac273c553a38f9252f60224da9fe62d9b60e9/Columnar-1.4.1-py3-none-any.whl", hash = "sha256:8efb692a7e6ca07dcc8f4ea889960421331a5dffa8e5af81f0a67ad8ea1fc798", size = 11845, upload-time = "2021-12-27T21:58:54.388Z" }, +] + [[package]] name = "coverage" version = "7.14.1" @@ -1776,6 +1817,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" }, ] +[[package]] +name = "py-walk" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sly" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/b5/e2f3fab1e11d4089b1c3dfd72175fdb2408ff8028e01bdb0d308923609bb/py_walk-0.3.3.tar.gz", hash = "sha256:a1b28d6079f27203fa3098b69a98572675b3ff5bd02286c43e6dacd66615f879", size = 1815727, upload-time = "2024-10-26T14:30:39.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/38/56b67abdbf6797475dfe2f62d391b4a6ead851c76acbaf07e118e53651b6/py_walk-0.3.3-py3-none-any.whl", hash = "sha256:238fc018165138021ce0bfd9c351cdc473d3120ccc5534df35611b92608c94d5", size = 14537, upload-time = "2024-10-26T14:30:38.06Z" }, +] + [[package]] name = "pyasn1" version = "0.6.3" @@ -1888,6 +1941,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyjson5" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/9a/3db19560e968d6e85b2a4ddf4b949c6ebf9dd1dcfb5a9f37736f8adeb927/pyjson5-2.0.1.tar.gz", hash = "sha256:a5b0e322e847b198a50d8a1ef16d6b2b19129644dc018d76773e81ef1487ca39", size = 352242, upload-time = "2026-05-15T16:12:54.931Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/2c/0619f89a9576f335ba63eb851e73ba507480a8c7f28a2e7de2501ed3d303/pyjson5-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8a661d292801b38434d5288bf7329f9b50b3a693b1d2941c7595175842692eba", size = 301821, upload-time = "2026-05-15T16:09:28.721Z" }, + { url = "https://files.pythonhosted.org/packages/91/31/7824913ec71e7421d6a57bc06228f3e2d946d8e8f738f898572dded0dc57/pyjson5-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cb5c1c066038ce6e1922d9d5be23f5cd14d5b5a3c96e6358aa5d0e379014a93", size = 158201, upload-time = "2026-05-15T16:09:30.261Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7d/4ddb249563a838425242342d2cf67976ccc292a831b543a92fa1a8a83b11/pyjson5-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7417eab751817fa5f070e41975d9df4aec3532d21389073318161f63cd96d636", size = 153244, upload-time = "2026-05-15T16:09:32.04Z" }, + { url = "https://files.pythonhosted.org/packages/02/39/7622416ac0570d9ce377447bd5b2ec9383c1282e63cc6d0b65779f1336fa/pyjson5-2.0.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cc4802093b4ab9039367774988486fd04754cf416c32aebf94a94bafd8b8a478", size = 185568, upload-time = "2026-05-15T16:09:33.719Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/2f317da231477b77481020d79c73bbe10625e4925ceeae77603726ef1763/pyjson5-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc3181274ed19ecb2315cf85a499bf83002554f42c72453666c616059d665a7b", size = 163169, upload-time = "2026-05-15T16:09:34.865Z" }, + { url = "https://files.pythonhosted.org/packages/02/b3/20023c3cfe2f2c2523007d7031b5b7ad7ccd8856d2665547683cebaa6f8e/pyjson5-2.0.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5398306b8aa253e620af9ed8085767de9cd28d6846da45e535e80fcffa9f33d7", size = 166514, upload-time = "2026-05-15T16:09:36.383Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1c/b863e502153477b2845a54b688b72436457aca5107b300fca95e98184551/pyjson5-2.0.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b97b592287d52a6ec5af84dca94aab02b185f72f8ddb6fc99c58efb8891be5cd", size = 179907, upload-time = "2026-05-15T16:09:38.166Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b2/413fcb76632e5f6fa89a2ec83976755b5bb4696c6d9eca9ff3c70cd717b6/pyjson5-2.0.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4efbf5e910a3aa6f330a92d1ae940dda5ed662976ed5056526baaf2e1821f95d", size = 187171, upload-time = "2026-05-15T16:09:39.624Z" }, + { url = "https://files.pythonhosted.org/packages/d6/11/66151b819407ab589aef36582038257f1ef42dc065e3423b3d8274f4fcd2/pyjson5-2.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e042f9b869e7f21d5f53a2638e5d0cf1daaba2342127c8777c502daf972602e", size = 175564, upload-time = "2026-05-15T16:09:41.356Z" }, + { url = "https://files.pythonhosted.org/packages/f2/98/deb70690dab994474ea527cba91f43ae55928bcff498c441e812b60fc1e2/pyjson5-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:26074370d7a6dd38b6e8c28a2f10c790fc6dec938ec22ec3de6c93c97d195f10", size = 171530, upload-time = "2026-05-15T16:09:42.78Z" }, + { url = "https://files.pythonhosted.org/packages/a9/be/969752a3a052d00698b6e1dd926c627d7a6475a7c8dd390db148363544f8/pyjson5-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3cb163a495096716a7a01d052b60bda0308ecfaeddcb2d50a247ba9d707e3e40", size = 1145510, upload-time = "2026-05-15T16:09:45.005Z" }, + { url = "https://files.pythonhosted.org/packages/d0/be/4c5c92cdda5a911ee4450a74281782335e7ce6715638308322beb96be639/pyjson5-2.0.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5fdd66bdddc2d53421ab7427bedcd2ca9b2f1b4b3b464381aa60d305c94be38c", size = 1010114, upload-time = "2026-05-15T16:09:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/89/1e/72283bc505d77dcdab7eaeb0020cf0fd79a05d2991e886d1fcfa73e88ae1/pyjson5-2.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4ad222a3eff1cc93f9b70894c5f3d2ff5e46531e88feaa4818e329d5f3ae4307", size = 1322915, upload-time = "2026-05-15T16:09:48.885Z" }, + { url = "https://files.pythonhosted.org/packages/06/b4/94a09e744a6bb6e76108b61a28a7cc5ecab1b8105cacde7548359b3b74a3/pyjson5-2.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:63f5cf25113dd1bf3bfeda211d99413e58ec6b9036ff34f7b69f85f23695e9c4", size = 1240126, upload-time = "2026-05-15T16:09:50.638Z" }, + { url = "https://files.pythonhosted.org/packages/af/29/4549380cae425ee112f3c154606c35f5211c12ddf415f079c2b23f2d493b/pyjson5-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b0001eecce9080e6c170e978786501e8931e2075a3d15f5260e1bdd6a45e8976", size = 1178022, upload-time = "2026-05-15T16:09:52.639Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1a/5a8a869e855645858e45dbd077204cec3a85b2f2e669bc3c8cefe9c4a70e/pyjson5-2.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40371c73cc83ed81914028786a6cc5950bdf300ae82443df83a5dfa80c582fca", size = 1355161, upload-time = "2026-05-15T16:09:55.177Z" }, + { url = "https://files.pythonhosted.org/packages/82/32/827066cd946447648275a892f494f4572d78e6a79d877fa6b6c14af599d8/pyjson5-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c9dca542eeb6e8fbbc1fe3373bfd18ae6250f1aedc78f39242ac12c8837091b8", size = 1208864, upload-time = "2026-05-15T16:09:56.919Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cc/a9bc12aff47d8bfd3074a21e3fb056ec0028fac28f8ec7e0fe4449d050f5/pyjson5-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d30c8b2c91d530be475a8fab6dde8c3bcd3e89b999c9e0f05aec2bf2c24b0638", size = 115461, upload-time = "2026-05-15T16:10:30.711Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c7/fa3fc956fbc3fa6250c8b99ef75a8f52b691780fca1bfb368340283bd898/pyjson5-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:67f8f5b8d3e3b2ca5f618c928729986aa00fb588c476c0d0d3a151633ff41e0d", size = 135836, upload-time = "2026-05-15T16:10:32.062Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0a/e457b20d1e36a766d3ccd09c418b7ef41acabb679ff2248bb1cd38247ce6/pyjson5-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:bdf30ce5b1242f63254d936b30af75cae237fab0ee71cc2544163c82b6bc9201", size = 116652, upload-time = "2026-05-15T16:10:33.39Z" }, + { url = "https://files.pythonhosted.org/packages/2b/4c/6266789e615576b62d2db6de26a3b4b8f4cc7a8ff23fe24e48363bca1682/pyjson5-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:954c14d40022fa40e0d36e0ebd337e9e90fd71145592d25a0e2868344e3daca0", size = 320487, upload-time = "2026-05-15T16:09:58.38Z" }, + { url = "https://files.pythonhosted.org/packages/dc/75/c4a563034805e20b274fe4db963e1d480001931d88fe70b7d082033b7dcb/pyjson5-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f57529b98d21e8b76f8bc51af9d3c3057dd04a92287fd21ef7ec55255ca6f5c5", size = 166879, upload-time = "2026-05-15T16:09:59.849Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/5e6ec3580379794e9a99a402465f74586e93e93814974172202d1254fa89/pyjson5-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:545b655ef0b59f39fc29e2b63b4dadd45e447ad77bad2fdca859ff4db69e21f5", size = 162235, upload-time = "2026-05-15T16:10:01.147Z" }, + { url = "https://files.pythonhosted.org/packages/5b/48/b2d0e868ef8375eb0696ddc74f71028fc4fcd26cdddaeb34485214f2dc87/pyjson5-2.0.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b519de49dd4cbf254179749e5c6bfe9bd8fd1f97e7755bff2e17cc6f79f85aa5", size = 182162, upload-time = "2026-05-15T16:10:02.523Z" }, + { url = "https://files.pythonhosted.org/packages/ce/18/fed02a3d68c3badb87394e8420b72d7cb165ade208c1e02561637409aa99/pyjson5-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de48fbd0466c114c17f408be7bcbc5b3aedeb29ff5f73187022915e6d5eb7817", size = 169487, upload-time = "2026-05-15T16:10:03.876Z" }, + { url = "https://files.pythonhosted.org/packages/8b/90/dc897332dda24e949828616e8e74d7937b587a56789f3aca148fcadddde2/pyjson5-2.0.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fe6516ff9ab94368c18ff8ac0ebdb53fac9e047da093d43e67d5e7cd98cdd0bf", size = 164128, upload-time = "2026-05-15T16:10:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/39/09/90104baeab58adfe63bda804174ef1806b1cabb1692e047fabc2f4aa1799/pyjson5-2.0.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ea7aadacac7ac6661117b708175f6beff75e132322c08bbf3794d8ec4ba572b4", size = 184306, upload-time = "2026-05-15T16:10:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/89/73/613efc3cdab5cf9ec399e6c5a0463b672e5f5cbd693a3a4d82cbdda19476/pyjson5-2.0.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e5c339bcac113dfc30a1f88f23af273d67bb6169abee1684c0dd27daa71a32f9", size = 192239, upload-time = "2026-05-15T16:10:08.727Z" }, + { url = "https://files.pythonhosted.org/packages/47/2a/17b97e02b37ab9353fc9713e56470be3d2539501ed18d6c676cfbcd0a57c/pyjson5-2.0.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:075de0f1e5e0ffee174a8941b5d9d8af632772a6e5eebe92fb0899db7f6ab7fa", size = 177652, upload-time = "2026-05-15T16:10:10.19Z" }, + { url = "https://files.pythonhosted.org/packages/1c/43/a05331ba88fd1aa09ca418548c6b06692e34b2a64b109bb363e01ecafdeb/pyjson5-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c463c9bf508c1d316191898a3c0bfc03dd3de67bdc02f480b0742ac5c859eaeb", size = 174860, upload-time = "2026-05-15T16:10:11.976Z" }, + { url = "https://files.pythonhosted.org/packages/2e/00/5d253751f4d27b7e63fdfd765041593fa588395f45b9cea496878d964448/pyjson5-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:54427c8cad0c1a516bad5b4457167cde1e7ef7ee563e28a944f59b3c7413b6a0", size = 1151556, upload-time = "2026-05-15T16:10:13.608Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f7/a30eed477295e92dfd59553063cf82bf510a7052efc7720c9c50d0082fdb/pyjson5-2.0.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:ca04839cecd364cb53de7ebcd81fd5915193707d968730ea1c8d4ff5c9c2f1a8", size = 1012330, upload-time = "2026-05-15T16:10:15.171Z" }, + { url = "https://files.pythonhosted.org/packages/9c/60/80818fad90a99336f8bfc686d76d0b2d7c8bb51e8de5d031620fa1a2d7a1/pyjson5-2.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13a2de8b14aeb576b1623c8af419510bf871b8d9cc2308db41a46bcc94d00f2f", size = 1320970, upload-time = "2026-05-15T16:10:16.863Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3f/090eb3e0971d067defb58bbfd738f74a09be495c6f4f00d5d76a10755bca/pyjson5-2.0.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c8a689f5252e9791bfbc8fa916999202556a1e8c7b24dfd72de1ad84c493e10a", size = 1244775, upload-time = "2026-05-15T16:10:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/61/38/e7546ad733affe51a5462bad21bceb4fd659baf24930bd141627ce56279b/pyjson5-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3dc0c2c80fd9d1e4c26f8b6c91a54d38b48648b93ae3f09355856c75fda3e460", size = 1182149, upload-time = "2026-05-15T16:10:21.696Z" }, + { url = "https://files.pythonhosted.org/packages/90/2c/016647580d8a82ee53b4cb9ddb96deb4b157ccea9624bba61de4aa2cd25c/pyjson5-2.0.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:13d78b3e2c60b81bd7e6b00e6ab24a664ba1fd64b5d86baa65c4cdfae1fff7ae", size = 1358579, upload-time = "2026-05-15T16:10:23.649Z" }, + { url = "https://files.pythonhosted.org/packages/01/45/8b84ff7a0c4d8c12a76e097c67af62abe986a18d1ab2540764dd6eaa0253/pyjson5-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a7b640dbeabbd7d975170f793f90a7500cc5d510aeae9364a1bc558bcc5336ed", size = 1211015, upload-time = "2026-05-15T16:10:25.4Z" }, + { url = "https://files.pythonhosted.org/packages/cf/48/d8f34de7a7319f5966bdfd10f133e689ba01f138d2040bf792f1ca8b18e1/pyjson5-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:46e7c5525034fde8abf3aaf100ed7660c1b618ccd3205544ed7c99e4c55cf201", size = 131660, upload-time = "2026-05-15T16:10:26.747Z" }, + { url = "https://files.pythonhosted.org/packages/06/aa/0bd437252134115a846ffc061078c85f8e8c325c86abcaa862c134d3c57e/pyjson5-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:207c00e7f9641e0358bdc848a1c0610f26f2e8bd51d73bd64e5a0ab3b725f1d8", size = 157717, upload-time = "2026-05-15T16:10:28.053Z" }, + { url = "https://files.pythonhosted.org/packages/24/61/7849b04a0dc78f73905189400ac99c45de8eec2ec451ce9c6990fb3588e5/pyjson5-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4917d5b6186bfa48ec4d85326e2a09769a5f7844963307ab2710429e1f743264", size = 126281, upload-time = "2026-05-15T16:10:29.378Z" }, + { url = "https://files.pythonhosted.org/packages/89/41/70aa1cb1fb0a3ac4c9b8cd405c0c85ba935c53fdfae6271b8eae364b92d1/pyjson5-2.0.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:573fecd7cad4e24d232053f9ebf331c80fe1b0ed6e2064f7614797a7f691e7ca", size = 303706, upload-time = "2026-05-15T16:10:35.163Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d9/caf44bf3d33b9502dc4b8ed5d0c7a8af8fbe33001e82414c0407f39f18bd/pyjson5-2.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a8ade508a046a5407a322c098e3b7e6033216b158a7746eb8962b7a4cdbf9248", size = 158915, upload-time = "2026-05-15T16:10:36.496Z" }, + { url = "https://files.pythonhosted.org/packages/3f/94/f2bab1ce2eac8f77e16dcd0a1ba39de20733a84b9961c3504af0dd68bceb/pyjson5-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4635e349bb1d1f1e854dc790f14be31b6cb198d6453848e8d86818104074f805", size = 154199, upload-time = "2026-05-15T16:10:37.902Z" }, + { url = "https://files.pythonhosted.org/packages/82/c4/d94573ca37486af3d6a72f2582844d7d490d8e55639e0cfba371ce91442f/pyjson5-2.0.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d274697f81f143abac12ce256a06b61635c30d0f8cc11eefae7182655dac9a5", size = 186060, upload-time = "2026-05-15T16:10:39.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/cc/c42e697def319b286fdcb912939c044cc94bd1cfc7338b6dbb566f817f29/pyjson5-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be4e2242e55a2651fd8696cfd67ec8e04f54f4ed6c089cceea2b63763a7516d8", size = 165796, upload-time = "2026-05-15T16:10:40.967Z" }, + { url = "https://files.pythonhosted.org/packages/3f/dd/f22a5f0e619ef22b8e32520a91bd92f815d50f9ec2d67cfe5974eab9476c/pyjson5-2.0.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af855c680feaa39cae4a44914ee2863eeae1549f37ce58069c747b3d4803999a", size = 165262, upload-time = "2026-05-15T16:10:42.391Z" }, + { url = "https://files.pythonhosted.org/packages/99/8b/90e22ecb12d51ccdc68325b6b051002e6103cc4600a335d9ed13828acdd9/pyjson5-2.0.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bb769d90516da904e6cf0ba58f3d5830f4a5804486b5e85c5ca42ba3146d187a", size = 181608, upload-time = "2026-05-15T16:10:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ab/237f9036eed73e08cf8861466d3d1182ef1c88506bd29955740dcadb24ff/pyjson5-2.0.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6484c14e07aa46abeb3cb2ff0204a765260763ad7cf3f87f601f26b20ed607f3", size = 189582, upload-time = "2026-05-15T16:10:45.367Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3d/3df8d5f003910a9291e5f04fa178f626e3c8553a5986dc62589ca74195ff/pyjson5-2.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc03673adb544324500d79b2acfe2ced038998b2aceb564d335de9cc238cbdb0", size = 176321, upload-time = "2026-05-15T16:10:47.134Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d9/a458a54f780bafec1b44ee3c27cb3007a83fb7bd4f1c17ac6bf519fd6151/pyjson5-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d714daf784bec2e14fc06c0c26e4a373a6157eaeddaed2d5a7b7b6ae2aabc33", size = 171956, upload-time = "2026-05-15T16:10:49.174Z" }, + { url = "https://files.pythonhosted.org/packages/78/d8/1010e0147c8862dc0881cf3656364d3d87a5e336d290fe4cb0b92223e14c/pyjson5-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5cf3ded356730e08b5941a16db6525efc0de26f35468ab09ad573056c4efc6e5", size = 1147679, upload-time = "2026-05-15T16:10:51.005Z" }, + { url = "https://files.pythonhosted.org/packages/19/2f/91c8d8cdb4a2e4bbe8f14b9b57d717988e14830f2ed4a4f08d503cb5edde/pyjson5-2.0.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cc00d4669fb4170c28b2c9ed3cd1c2af9f21727c55d6866030cc154f31f68747", size = 1008216, upload-time = "2026-05-15T16:10:53.252Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d7/25c3f8693660a35be7bb4eb71b0d10d9a6981e986723a4881d607f0c6462/pyjson5-2.0.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a77a91c2e019476345c03cde105da44100daaabc3fa56f82f2bd9eb2ebbcb698", size = 1323856, upload-time = "2026-05-15T16:10:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/82f0f83556ff68ec0ca1129ba9afeb14149cd421dc74823cdf10c209c95c/pyjson5-2.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9ae54e5d717982cb7a082c700ab1f7f965c6aedea6fcb003ec4bdeac4f02bf52", size = 1241860, upload-time = "2026-05-15T16:10:57.635Z" }, + { url = "https://files.pythonhosted.org/packages/c1/01/2c4695fc06a0f3f29041a875d3bb24f904c9ae7b3d8dbfa1f8db17ce995c/pyjson5-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ce4f5b67b3a6fab623a31d83416b51a5b2c97cb172194f214078484bbb4783a6", size = 1178443, upload-time = "2026-05-15T16:10:59.398Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ae/c10f534037b096aca21b90017ac9d41f791572dc3ec31fcd92db8ddcd256/pyjson5-2.0.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f8d2e61f7bed0b40cfe5f62375581bf56d7b5b9707c9d74747b5931149d7d376", size = 1357739, upload-time = "2026-05-15T16:11:01.189Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f0/655224f78ede087aa6a295c4745423e0104dd31f67928675b1a4cea72a0a/pyjson5-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2930140edd1a64eb42689993c5ae317bca2e6bc785b5dc5053bd5e9d184c98ad", size = 1209542, upload-time = "2026-05-15T16:11:03.364Z" }, + { url = "https://files.pythonhosted.org/packages/73/52/a7b2a26625136fcd0f92e17beaaae51e04923cc2e28502db354d4de061b4/pyjson5-2.0.1-cp314-cp314-win32.whl", hash = "sha256:51733f91dda897239ca10928f363ce6f4b5eadaf797e74477f6c1c3222c185a7", size = 118342, upload-time = "2026-05-15T16:11:40.009Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8e/79db426fff3a41610076989d4060cc18a6508ebd3c7877155f6709eea102/pyjson5-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:698c73aacff49ea35bbbf7f700a97785626201ea7aa2e1f1e0a4fe23788c3be2", size = 138408, upload-time = "2026-05-15T16:11:41.757Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1b/385da14c05412bca15e86551dde91959686c8bddf7e78722bdc627fa6815/pyjson5-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:d447e2e5756f89abfd0ad82d1438075bcbc701c1cc9279b43d24825aaac67356", size = 120905, upload-time = "2026-05-15T16:11:43.146Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/cd69739556d304d3ea48064ea7d47e5ff7321b0032d7ad78864bceaa0cae/pyjson5-2.0.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5603be4f0cb9685d7c2cbd408ddec21b33f415252bee00ac7969f20b5789a1d1", size = 321150, upload-time = "2026-05-15T16:11:05.07Z" }, + { url = "https://files.pythonhosted.org/packages/84/d4/6d98268ed07a2cac1e79634fcd3dc280d2503ade5f3c465f7aa095951444/pyjson5-2.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:87267520a256b3f2dba7f2995b745e8b3f2dd72bbf8436010a5191f7809f8ab6", size = 167540, upload-time = "2026-05-15T16:11:06.822Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c6/8855a9462bdbc8306cba62793e23cc2952dfcbcbcab8e0413f570c891361/pyjson5-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:77e58307b74019d90aff9f2781bcffbbea3315b15fc814f8fa9b8d334b956ff4", size = 162533, upload-time = "2026-05-15T16:11:08.218Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a3/1092f68538ee71ed8027393ab5e71aa9c4a114e493e30a55e46e34c2ffb9/pyjson5-2.0.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dd1712038837342dea73ed526883e53c6e85a158242b6f574b9ad9cdd3199c7f", size = 184165, upload-time = "2026-05-15T16:11:09.639Z" }, + { url = "https://files.pythonhosted.org/packages/46/63/bb9f42a4047284ea59064c134c88dfac3b7f5273e40fe548d6a086cea452/pyjson5-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dba36677c77aed5d680827c65478933705ca156a34838f1250ea191ffc27662", size = 170075, upload-time = "2026-05-15T16:11:11.105Z" }, + { url = "https://files.pythonhosted.org/packages/55/2a/0379aa5184e986d0f9af2919712c148d81928ce7d2d7950407e271b1df3b/pyjson5-2.0.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:04356d0c09e58907f60675d33859f269473479b90add5026231ef2fadba5207e", size = 164100, upload-time = "2026-05-15T16:11:12.909Z" }, + { url = "https://files.pythonhosted.org/packages/15/d5/2087af69695c28c7fe796afc73dec7f0b0b9bc123ec329e45928d58d4705/pyjson5-2.0.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b331a0fbe2ae26f4ccb88045ebaecc00aeeeab23ff858f9a2223cedd969ec6d6", size = 184674, upload-time = "2026-05-15T16:11:14.448Z" }, + { url = "https://files.pythonhosted.org/packages/2a/06/cf1b2744e07f1689cb0ac663e162a060b81cd358474754fe9f3bf02d5398/pyjson5-2.0.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ca55878d923ba5764254ad1c020c22b33e6161b3555d1f457b8060f2b3c6c17", size = 193047, upload-time = "2026-05-15T16:11:15.969Z" }, + { url = "https://files.pythonhosted.org/packages/54/e6/a6b0deb6a3f393907c6d9115a7ac8e27557109204b56b05d3b57c51fc2ca/pyjson5-2.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35e18d11e4c2034b78a664affe18c153a8efbf8872cf6bed7f12dc4fb819d440", size = 178712, upload-time = "2026-05-15T16:11:17.364Z" }, + { url = "https://files.pythonhosted.org/packages/6e/48/9e5c16daba56dfdb481baab85d1197e14ae3f403902fc23cdca14d14351b/pyjson5-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b710a7489b6e134890eaa1fdb285e9644a1e730c1cfcf1cd90a87859b87a84a9", size = 175642, upload-time = "2026-05-15T16:11:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/c3/32/f07d4dbd8cf733f9e3f3e9c9abedfbc23da1e5cfa7fed46aebae07ca6f25/pyjson5-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:08920c0dd6aba3fb6bc6c849e6f05731c5fb2716cd651de424ff60cdd12eae65", size = 1152219, upload-time = "2026-05-15T16:11:20.54Z" }, + { url = "https://files.pythonhosted.org/packages/01/9d/0ddbb89d381bc27f3740850d5d664c1f6f76620ad879ff5eb2506a4dfd6c/pyjson5-2.0.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:e166dadb3275025cff5ee8602131372a3ea171c6727b3fc6e44697348e3972c5", size = 1013305, upload-time = "2026-05-15T16:11:22.702Z" }, + { url = "https://files.pythonhosted.org/packages/bb/93/0f44886391dd2249ad9cf98c2deb3f26d1a115dd5a215629af7455765968/pyjson5-2.0.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ea3c2b4e7b8e209e7f59bd6a925d79b69cd0a4ffa12f2df0c6af55514c8b2227", size = 1322840, upload-time = "2026-05-15T16:11:24.918Z" }, + { url = "https://files.pythonhosted.org/packages/d4/29/c478dc24dfcfc07e407ff8669df265ecc51fed5e7d47e51a01f17fd1c53f/pyjson5-2.0.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b710fc8ce00c984c8865131f05a8536163e1d3caffb9d081647879eca7424670", size = 1245221, upload-time = "2026-05-15T16:11:26.873Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f0/5fef8c47c5b7052da79af6425a021040402a9a09b37c5687d91f98696a27/pyjson5-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9784d79275d1b06d95f2920c3f260ce5d3f6671f65a1fae1464f83fbfc834a0", size = 1182991, upload-time = "2026-05-15T16:11:29.258Z" }, + { url = "https://files.pythonhosted.org/packages/90/33/4d3a9d3159cdff1f1886d39f1991e6c8d5927f9b606cfc9743b5bef98c2a/pyjson5-2.0.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0ef86c53d0e14991b5b0fe58f225e3d8faec437aa0a70da4762525b6bc2fca10", size = 1359104, upload-time = "2026-05-15T16:11:31.651Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/20eb16c52453aea0af68a697f4058378c9ff871bd2f0bea28eb76ffe12f8/pyjson5-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1d9faccf5a9e86f14104eec31c13428ef8baeb867182ce107e0c68fcc5c62477", size = 1212847, upload-time = "2026-05-15T16:11:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a3/69bbe93275eafb7802a5b29230cc5c28f9b924c68d15b3ab2d43cfe19825/pyjson5-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:2df497af7ab03cf82d9278a4707a83d0ae4e6d727f919a883b5ccec3f7d92650", size = 139137, upload-time = "2026-05-15T16:11:35.487Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/b865ca6e0ae6887bc2b6b17cd123470ae43ac0fa04a2362d77d4b9f5bd43/pyjson5-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:3b31cf4f4a4f01800812865af3b02f6700cefb9377e4d9a9c6b78fdcf41fd973", size = 169412, upload-time = "2026-05-15T16:11:36.887Z" }, + { url = "https://files.pythonhosted.org/packages/68/55/02c734459fef0a955ab3e7e745df415d5f9fc873a4c288765f60f22fbc13/pyjson5-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:15f0d8baea89d35c6c01c60b944c778a79cac10f77f71b727f1b244e2c7a8ccb", size = 129061, upload-time = "2026-05-15T16:11:38.712Z" }, +] + [[package]] name = "pyjwt" version = "2.13.0" @@ -1902,6 +2043,22 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pymarkdownlnt" +version = "0.9.38" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "application-file-scanner" }, + { name = "application-properties" }, + { name = "columnar" }, + { name = "py-walk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/98/c4ef7b1bac73ebc90418c29a89c3c81b2d4595e50683d7a85bc6a6513fe3/pymarkdownlnt-0.9.38.tar.gz", hash = "sha256:bd79a2cef3a90652135672272b20aabb8f5db8dd870983ee8710f7daf64a74a7", size = 442175, upload-time = "2026-06-11T03:28:31.693Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/d1/eee0e6d73cf9b4d20af1ec42583999fd5af4952fdc32276734ad9fbbb737/pymarkdownlnt-0.9.38-py3-none-any.whl", hash = "sha256:37eda39c2ba0d5be10abe0d1150e4564b08cd26a3570b1575a972db5a423aa39", size = 515479, upload-time = "2026-06-11T03:28:30.03Z" }, +] + [[package]] name = "pymdown-extensions" version = "10.21.3" @@ -2310,6 +2467,7 @@ docs = [ { name = "mkdocs" }, { name = "mkdocs-material" }, { name = "mkdocstrings", extra = ["python"] }, + { name = "pymarkdownlnt" }, ] [package.dev-dependencies] @@ -2347,6 +2505,7 @@ requires-dist = [ { name = "pip-audit", marker = "extra == 'dev'" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "pymarkdownlnt", marker = "extra == 'docs'" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "pytest-asyncio", marker = "extra == 'dev'" }, { name = "pytest-cov", marker = "extra == 'dev'" }, @@ -2497,6 +2656,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sly" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/8a/59e943f7b27904c7756a7b565ffbd55f3841f5cd3d2da2b2b0713c49e488/sly-0.5.tar.gz", hash = "sha256:251d42015e8507158aec2164f06035df4a82b0314ce6450f457d7125e7649024", size = 66702, upload-time = "2022-10-25T14:35:30.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/4d/c96d807295183f2360329cd8d8bf5e8072c53d664125b3858c04153f026e/sly-0.5-py3-none-any.whl", hash = "sha256:20485483259eec7f6ba85ff4d2e96a4e50c6621902667fc2695cc8bc2a3e5133", size = 28864, upload-time = "2022-10-25T14:35:28.054Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -2705,6 +2873,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] +[[package]] +name = "toolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, +] + [[package]] name = "tqdm" version = "4.68.3"