15 KiB
Repository Guidelines
Project Overview
roboomp is a self-hosted GitHub triage-and-fix bot that drives omp --mode rpc as a subprocess. On every issue opened in an allowlisted repository it classifies the issue, applies labels, then branches into one of: reproduce → fix → PR (bug / documentation), single-comment answer (question), single thoughtful comment (enhancement / proposal), or brief comment (invalid / duplicate). Follow-up comments and PR review comments resume the same omp session so the agent keeps its prior reasoning. If the orchestrator restarts mid-task, the dispatcher resumes the same session via omp --continue from the per-issue session_dir, so an interrupted task re-enters its prior reasoning instead of restarting from scratch. The orchestrator runs as a single FastAPI process inside Docker with SQLite-backed durable event state.
Architecture & Data Flow
Webhook → durable queue → async dispatcher → per-issue git worktree → omp RPC subprocess + host tools.
POST /webhook/github— HMAC-SHA256 verified againstGITHUB_WEBHOOK_SECRET(server.py+github_events.verify_signature). Bad signature returns401.github_events.route()decides one oftriage_issue/handle_comment/handle_pr_conversation/handle_review/cleanup_workspace/skip. Bot-authored events (*[bot],user.type == "Bot", configuredbot_login) and non-allowlisted repos are dropped here.db.record_event()inserts the event withINSERT OR IGNOREonX-GitHub-Delivery(dedup). Endpoint returns202.queue.WorkerPool._dispatch_loopatomically claimsstate='queued'rows underBEGIN IMMEDIATE, guarded by an in-process_inflightset keyed by(owner, repo, number)to serialize per-issue work. Cap:ROBOMP_MAX_CONCURRENCY(default 8).sandbox.SandboxManager.ensure_workspace()produces a worktree at/data/workspaces/<owner>__<repo>__<n>/repoon a deterministic branchfarm/<8hex>/<slug>, backed by a shared--filter=blob:noneclone pool. Credentialed remote URL and git identity are reset every time.tasks.*dispatchers buildTaskInputsand callworker.run_task()which spawnsomp --mode rpcwithcwd=worktree, persistentsession_dir, and a randomly-picked model fromROBOMP_MODEL(CSV pool). When<session_dir>/*.jsonlalready exists the worker passes--continue, so both follow-up events and crash-restarted events resume the same session.- Inside the subprocess the agent uses built-in omp tools (read/edit/write/bash/lsp, scoped to the worktree) and host tools from
host_tools.py(the only surface allowed to mutate GitHub or write audit rows). - Success → event
state='done'. Exception →state='failed'with a credential-redacted traceback inevents.last_error. The_inflightslot is released either way.
Key Directories
src/— package (see "Important Files").src/prompts/— Mustache-style{{var}}templates loaded bypersona.pyvia@cacheandimportlib.resources. Shipped as package data (pyproject.tomlpackage-data).tests/— pytest suite.test_worker_smoke.pyis gated onROBOMP_INTEGRATION=1.data/— runtime state (sqlite + WAL,workspaces/,logs/). Never committed./Dockerfile(pi root) — producesoh-my-pi/pi:dev(pi runtime image: python + bun + rustup + pi-natives + omp_rpc +/usr/local/bin/ompshim + the full pi source under/pi). Stages:natives-builder→wheel-builder→pi-base→pi-runtime(default). Built viabun run pi:image. Robomp's image extendspi-baseviaFROM ${PI_BASE}in/Dockerfile.robomp.
Development Commands
Task runner is bun against the monorepo root package.json. roboomp itself no longer ships a package.json; every recipe lives at the root under the robomp:* namespace. Local venv (no docker): bun run robomp:install runs pip install -e 'python/robomp[dev]'. From there:
bun run test:py # pytest -x python/omp-rpc/tests python/robomp/tests
bun run robomp:test:integration # ROBOMP_INTEGRATION=1, requires omp on PATH
bun run robomp:serve # python -m robomp serve on the host
Docker inner loop:
bun run pi:image # build oh-my-pi/pi:dev (one-time / on pi change)
bun run pi:run # docker run -it oh-my-pi/pi:dev (smoke-test the shim)
bun run robomp:build # pi:image (if pi changed) + docker compose build
bun run robomp:dev # build + up -d + follow logs
bun run robomp:up / robomp:down / robomp:restart / robomp:logs
bun run robomp:rebuild # docker compose build --no-cache
bun run robomp:reset # `down -v` + drop the pi image
Frontend (Vite + SolidJS, in web/ — still a bun workspace):
bun run robomp:web:dev # vite dev server with proxy to :8080
bun run robomp:web:build # produce src/static/ bundle
bun --cwd=python/robomp/web run typecheck # tsc --noEmit
In-container CLI (robomp console script → robomp.cli:main): no root aliases — invoke directly:
docker compose --project-directory python/robomp exec robomp robomp triage owner/repo#N
docker compose --project-directory python/robomp exec robomp robomp replay <delivery_id>
docker compose --project-directory python/robomp exec robomp robomp status
docker compose --project-directory python/robomp exec robomp robomp cleanup owner/repo#N
HTTP / sqlite / webhook inspection is unaliased — use curl http://localhost:${ROBOMP_BIND_PORT:-8080}/{healthz,readyz,events,issues} and docker compose --project-directory python/robomp exec robomp sqlite3 /data/robomp.sqlite directly.
Lint + format: TypeScript via Biome (config in biome.json), Python via Ruff (config in pyproject.toml). Root recipes cover both languages — bun run lint / bun run fix apply to the whole monorepo including roboomp. bun run lint:py / bun run fix:py scope to Python only.
Code Conventions & Common Patterns
- Python ≥3.11, container is 3.12-slim.
from __future__ import annotationsis the norm; type hints are mandatory on public functions. - Records: prefer
@dataclass(slots=True, frozen=True)for immutable value types (seegithub_client.IssueInfo,sandbox.Workspace,db.EventRow). - Async style: FastAPI handlers and
queue.WorkerPoolare async.worker.run_taskis synchronous and runs in a worker thread becauseomp-rpcis blocking — keep it that way; don't try to async it. CLI commands wrap withasyncio.run. - Config:
pydantic-settingsSettingsinconfig.pywithROBOMP_*env prefix (e.g.ROBOMP_MAX_CONCURRENCY,ROBOMP_REPO_ALLOWLIST). Access only viaget_settings()(@cachesingleton). Tests must callreset_settings_cache()after mutating env. - Dependency injection: pass
Settings,Database,GitHubClient,SandboxManagerexplicitly intocreate_app(),WorkerPool, andToolBindings. No module-level globals other than the singleton accessors (get_settings,get_database). - State: SQLite (
db.Database) is the source of truth forevents,issues,tool_calls. Thread-safe via an internal_lock;BEGIN IMMEDIATEfor claim contention. In-memory state is only the_inflightset inWorkerPool. - Error handling: custom exception types (
GitHubErrorwithretry_after,GitCommandError,InvalidIssueRef,RpcCommandError).sandbox.redact_credentials()stripsuser:pass@from any URL before it lands in logs, audit rows, or exception messages. Never include credentialed URLs in error strings. - Logging: structured JSON via
logging_config.JsonFormatter. Uselogger.info("event", extra={...}); do not collide with_RESERVEDkeys. Configure once viaconfigure_logging(). - Host tools (
host_tools.py): every tool is built from a per-taskToolBindingsclosure and audits through_audit()intotool_calls. Audit only ever sees agent-supplied args, never internal credentials. New tools follow the same pattern: validate args → callGitHubClient/SandboxManager→ return structured dict → audit. - Naming: snake_case for everything Python; module names singular nouns; test files
test_<module>.py; test functionstest_<action>_<condition>. - Prompts: edit
src/prompts/*.md. Variables use{{path.to.field}}; resolution ispersona._lookup. The package install includes them as data files — adding a new prompt requires no other registration.
Important Files
src/server.py— FastAPI app,/webhook/github,/healthz,/readyz,/events,/issues, manual triage/replay endpoints, dashboard at/.src/queue.py—WorkerPooldispatcher and_inflightserialization.src/tasks.py— the five task entry points the dispatcher calls.src/worker.py— synchronous omp RPC driver, prompt assembly viapersona.src/host_tools.py— agent's GitHub surface; tool list:classify_issue,set_issue_labels,gh_post_comment,repro_record,gh_push_branch,gh_open_pr,gh_request_review,mark_unable_to_reproduce,abort_task,fetch_issue_thread.src/sandbox.py— clone pool + worktree lifecycle,GitCommandError, credential redaction.src/github_client.py— typed httpx client; parses webhook payloads intoIssueInfo/CommentInfo/PullRequestInfo.src/github_events.py— routing and HMAC verification.src/db.py— sqlite schema and DAOs (record_event,claim_next_event,upsert_issue,log_tool_call).src/config.py—Settingsmodel andget_settings().src/cli.py— Click CLI (serve,triage,replay,status,cleanup).src/dashboard.py— single-page HTML dashboard served from/.pyproject.toml— packaging + pytest config (asyncio_mode = "auto",testpaths = ["tests"])./Dockerfile.robomp(pi root) — robomp's image.FROM ${PI_BASE}(defaultoh-my-pi/pi:dev), adds the SolidJS dashboard bundle, the robomp Python package, and therobomp-entrypointshim. Tini entrypoint, exposes8080,VOLUME /data. The toolchain (python + bun + rustup + pi-natives + omp_rpc +ompshim) comes frompi-base— no duplication in this file.docker-compose.yml—build.args.PI_BASE, mounts$PI_ROOT:/work/pi:ro,./data:/data,~/.omp/agent/models.container.yml:ro(mapped tomodels.ymlinside the container — kept separate from the host's~/.omp/agent/models.ymlso the host omp doesn't pick up gateway routing intended only for the container),extra_hosts: llm-gateway.internal:host-gateway.entrypoint.sh— validatesPI_ROOT, creates/data/{workspaces,logs}+ build caches..env.example— authoritative list of required runtime env vars.README.md— full architecture + operational reference. Authoritative for end-to-end flow, host-tool spec, security posture, and configuration reference.
Runtime/Tooling Preferences
- Python: 3.11+ source target, 3.12 in container. Setuptools src layout (
pyproject.toml[tool.setuptools] package-dir = { "" = "src" }). - Package manager:
piponly. No poetry / uv / pdm files; don't introduce one. - Task runner:
bun(rootpackage.jsonscripts). Always reach for an existingbun runrecipe before invokingdocker composeorpytestdirectly. - Container runtime: Docker Compose v2. The image embeds Bun 1.3.14 + a rustup launcher and exposes
ompvia a/usr/local/bin/ompshim;ROBOMP_OMP_COMMAND=ompshould not need changing. - Required env (set in
.env, see.env.example):GITHUB_WEBHOOK_SECRET,ROBOMP_BOT_LOGIN,ROBOMP_GIT_AUTHOR_NAME,ROBOMP_GIT_AUTHOR_EMAIL,ROBOMP_REPO_ALLOWLIST, plus model knobs (ROBOMP_MODEL,ROBOMP_THINKING, optionalROBOMP_PROVIDER) and rate-limit / concurrency / timeout overrides. SetROBOMP_BOT_LOGINto the lowercase mention handle (roboompin production, no leading@or[bot]; config normalizes common variants).ROBOMP_MAINTAINER_LOGINSis optional comma-separated bare logins (@/[bot]optional, case-insensitive) for non-owner implementation authorizers. GitHub auth is mode-exclusive: either setROBOMP_GH_PROXY_URL+ROBOMP_GH_PROXY_HMAC_KEY(gh-proxy mode; PAT lives only in the sidecar container — the bundled compose default), or setGITHUB_TOKENdirectly (single-process PAT mode).Settings._validate_proxy_or_patrejects a.envthat sets both. - PI_ROOT resolution: roboomp lives inside the oh-my-pi monorepo at
python/robomp/.bun run pi:imagebuilds the parent monorepo (../..) as its docker build context to produceoh-my-pi/pi:dev;docker-compose.ymlextends that image viaPI_BASEand mounts the same parent path read-only at/work/pifor the orchestrator to see live source. OverridePI_ROOTonly when pointing the build/mount at a different oh-my-pi checkout. Inside the container the path is always/work/pi. Build invalidation stays bounded: Python-only edits in roboomp never trigger a natives recompile. - Forbidden: no docker-in-docker, no extra service containers, no new background workers outside
WorkerPool. The container itself is the isolation boundary; per-issue isolation is the git worktree.
Testing & QA
- Framework:
pytestwithasyncio_mode = "auto"(pyproject.toml). HTTP mocking withhttpx.MockTransport;respxis available but onlyMockTransportis used in-tree — match that style. - Fixtures (
tests/conftest.py):env—monkeypatch-sets all requiredROBOMP_*env vars and callsreset_settings_cache()before/after.settings— invokesensure_paths()for sqlite/workspace dirs.db— isolatedtmp_path/test.sqliteDatabase; tests mustdatabase.close()in teardown when bypassing this.
- Isolation rules: any test mutating env via
monkeypatch.setenvMUST also callreset_settings_cache()to invalidate the@cachedget_settings(). - Async tests:
test_github_client.pyandtest_host_tools.pyspin custom event loops in background threads to bridge sync-style tests with async client code. Preferpytest-asyncioautomode (async def test_*) for new tests; only fall back to the loop helpers if matching the surrounding file's style. - Mocking: never patch internals; inject test doubles via
httpx.MockTransportfor HTTP and via thedb/tmp_pathfixtures for storage. Sandbox tests use a real local bare repo as the upstream. - Integration:
tests/test_worker_smoke.pyis gated byROBOMP_INTEGRATION=1(usespytestmark.skipif) and needsomponPATH. Don't enable it in defaultbun run test:py. - Coverage expectation: ~80 unit tests currently. New code with a control-flow branch needs a test covering it; new host tools need at minimum a happy path + one validation-failure path mirroring
test_host_tools.py. Test logical behavior (assertions on observable effects in DB / HTTP requests), not literal strings or default config values.