An AI dark factory: label a GitHub issue and the service drives the Hermes Agent CLI through the entire delivery lifecycle — planning, implementation, self-review, and a draft PR — with a human only ever approving the plan and the final PR. No human writes code.
issue labeled ──▶ PLAN ──▶ (human approves plan) ──▶ IMPLEMENT ──▶ SELF-REVIEW ⇄ REVISE
▲ │ │
└─ feedback ──┘ (confidence ≥ threshold)
│
▼
(human approves PR) ◀── AWAIT PR REVIEW ◀── OPEN DRAFT PR
│ ▲
▼ └── changes requested ──▶ IMPLEMENT
DONE
- Trigger. An issue is labeled with the trigger label (default
hermes). AJobis created. - Plan. Hermes reads the issue (in a clone of the repo) and posts an implementation plan as an issue comment.
- Plan approval loop. A maintainer replies
/hermes approveto proceed, or leaves a comment with corrections — each comment re-plans until approved (capped byMAX_PLAN_REVISIONS). - Implement. Hermes writes the code on a branch (
hermes/issue-<n>). An optionalVERIFY_COMMAND(tests/build) gates the work, iterating up toMAX_IMPLEMENTATION_ITERATIONS. - Self-review. Hermes reviews its own diff and returns a JSON verdict
{confidence, verdict, issues[]}. BelowREVIEW_CONFIDENCE_THRESHOLDit revises and re-reviews, up toMAX_REVIEW_PASSES. - Draft PR. The branch is pushed and a draft PR is opened, linking the issue.
- PR approval loop. Approve the PR review → the job is
DONE. Request changes → the implementation loop runs again and pushes an update.
The orchestrator owns git; Hermes only edits files. Prisma (SQLite) is the source of truth for every job's state, plan revisions, agent runs, reviews, and the work queue — each Hermes prompt is rebuilt deterministically from the database.
- NestJS (ESM,
"type": "module", NodeNext) — modular service. - Prisma + SQLite — file-based; no DB server to provision.
- DB-backed queue — a polling worker claims tasks with an atomic
UPDATE ... RETURNING(SQLite-safe), with exponential backoff/retry and per-job concurrency isolation. - GitHub App — webhooks, scoped installation tokens, comments, draft PRs, reviews.
- Hermes Agent CLI — invoked headless:
hermes -z --yolo --source tool --max-turns N.
Module layout (one service per module): config, prisma, metrics, health,
github, webhook, job, queue, worker, agent, workspace,
review, orchestrator.
This is an npm-workspaces monorepo, driven from the root:
api/— the orchestration service (this is the whole product today).app/— a management/overview UI (planned; not yet present).
Root scripts fan out to the workspaces: npm run build, npm run lint,
npm run typecheck, npm test, npm run test:e2e, npm run dev, plus the
prisma:* and hermes:* helpers. See the root package.json.
cp api/.env.example api/.env # fill in GitHub App + Hermes values
npm run setup # installs all workspaces + generates client + migrates
npm run dev # runs the api service in watch modeProbe it:
curl localhost:3030/health # liveness
curl localhost:3030/health/ready # readiness (DB)
curl localhost:3030/metrics # Prometheus metricsThese can't be scripted for you and gate the live run (not the build or tests):
- Permissions: Issues Read & write, Pull requests Read & write, Contents Read & write, Metadata Read-only.
- Subscribe to events: Issues, Issue comment, Pull request review, Installation.
- Webhook URL:
https://<your-host>/webhooks/githuband a webhook secret. - Generate a private key (PEM). Install the App on the target repos.
- Put the values in
.env:GITHUB_APP_ID,GITHUB_WEBHOOK_SECRET, and eitherGITHUB_APP_PRIVATE_KEY(inline,\n-escaped) orGITHUB_APP_PRIVATE_KEY_PATH.
For local dev, tunnel webhooks with smee.io or ngrok to
localhost:3030/webhooks/github.
Hermes keys all of its config, credentials, sessions, and state off HERMES_HOME. The
setup script gives the orchestrator a project-local HERMES_HOME so it never reads or
writes your global ~/.hermes:
npm run hermes:local # default: reuse your system `hermes` binary, isolated HERMES_HOME
npm run hermes:docker # strongest: build the agent sandbox image, run agents in a containersystemmode (default): uses thehermesalready on your PATH but pointsHERMES_HOMEat./.hermes/home, so any local Hermes config/context stays out of orchestration. No Docker, no reinstall.dockermode: buildsapi/Dockerfile.agentand runs every agent invocation in a container (SANDBOX_MODE=docker), mounting only the job directory and the isolatedHERMES_HOME(read-only). True isolation; requires Docker.
The script writes HERMES_BIN / HERMES_HOME / SANDBOX_MODE into api/.env and creates
the isolated home. It does not copy your credentials — configure the isolated home
separately (the script prints the exact command), e.g.:
HERMES_HOME="$(pwd)/.hermes/home" hermes model # provider setup wizard, scoped to the isolated home
# …or set HERMES_PRIMARY_MODEL / HERMES_PRIMARY_PROVIDER + an API key in api/.envCamofox is a self-hosted Firefox-based
browser server with fingerprint spoofing. When configured, Hermes's browser tools
(browser_navigate, browser_click, etc.) route through it instead of cloud providers
like Browserbase.
Start Camofox (Docker, port 9377):
git clone https://github.com/jo-inc/camofox-browser
cd camofox-browser
make up # builds + starts on port 9377Wire it to Olympian — add to api/.env:
CAMOFOX_URL=http://localhost:9377That's all. In SANDBOX_MODE=docker the orchestrator automatically rewrites the URL to
host.docker.internal:9377 before passing it into the agent container, and the baked
hermes config (api/.hermes/config.yaml) already enables loopback URL rewriting so the
agent can reach host-side services when navigating pages.
To opt out: leave CAMOFOX_URL empty (or unset) — Hermes falls back to agent-browser
(a local Chromium install) or errors if no browser backend is available.
- Label an issue
hermes(or yourTRIGGER_LABEL). - Hermes posts a plan. Reply with comments to iterate, or
/hermes approveto build. - A draft PR appears. Approve the PR review to finish, or request changes to loop.
Issue-comment commands (maintainers only — write access required):
/hermes approve— approve the plan and start implementation/hermes cancel— stop the job/hermes status— report current state
SANDBOX_MODE=none(default) runshermesas a subprocess in the job's worktree.SANDBOX_MODE=dockerruns each agent invocation insideDOCKER_AGENT_IMAGE, mounting only that job's directory and the read-only Hermes config. Each job lives in its own directory keyed by job id, so raisingWORKER_CONCURRENCYruns N isolated jobs in parallel.
All config is validated at boot (see src/config/config.model.ts). Full reference and
defaults live in api/.env.example. Key knobs:
| Variable | Purpose |
|---|---|
TRIGGER_LABEL |
Label that starts a job (default hermes). |
REVIEW_CONFIDENCE_THRESHOLD |
Min self-review confidence to open a PR (default 85). |
MAX_PLAN_REVISIONS / MAX_IMPLEMENTATION_ITERATIONS / MAX_REVIEW_PASSES |
Loop caps. |
WORKER_CONCURRENCY |
Parallel jobs (default 2). |
VERIFY_COMMAND |
Optional tests/build command used as an acceptance gate. |
SANDBOX_MODE |
none or docker. |
HERMES_BIN / HERMES_HOME / HERMES_PRIMARY_MODEL / HERMES_MAX_TURNS |
Hermes invocation. |
HERMES_REVIEW_MODEL / HERMES_REVIEW_PROVIDER |
Optional independent model for the self-review cycle. |
cd api && cp .env.example .env # fill in values
docker compose up --build # from repo root: builds the api imageSQLite lives on a named volume; there is no separate database container.
Run setup.sh once to register the olympian systemd service under your current user:
./setup.shThe script detects your Node/npm paths automatically and writes
/etc/systemd/system/olympian.service, then enables it to start on boot. It uses sudo
only for the two privileged steps (writing the unit file and systemctl daemon-reload).
After running the script:
sudo systemctl start olympian # start the service
sudo systemctl status olympian # check it is running
sudo journalctl -u olympian -f # follow logsOn every start the service runs npm ci, npm run setup (Prisma migrate), npm run build,
and npm prune --omit=dev before launching the production server, so deployments are as
simple as git pull && sudo systemctl restart olympian.
To uninstall:
sudo systemctl disable --now olympian
sudo rm /etc/systemd/system/olympian.service
sudo systemctl daemon-reload- Health:
/health(liveness),/health/ready(DB). - Metrics:
/metrics— job counts by state, queue depth, agent run durations, webhook counts, last review confidence. - Audit trail: every state transition (
JobStateTransition) and every Hermes invocation (AgentRun) is persisted; inspect withnpx prisma studio. - Retries: failed tasks back off exponentially up to
QUEUE_MAX_ATTEMPTS; exhausted jobs are failed and a comment is posted. Orphaned tasks are reclaimed afterQUEUE_LOCK_TTL_MS.
cd api
npm test # unit
npm run test:e2e # full webhook→plan→approve→implement→review→PR loop with a stub HermesThe e2e suite stubs the Hermes agent and fakes the GitHub API, so the entire pipeline runs in CI without real LLM or GitHub credentials.