API server that runs Cursor agents against local repository workspaces. Remote IDE clients delegate work via HTTP — prompts are executed on this host against checked-out repos under a configurable root directory.
Built on the Cursor TypeScript SDK (@cursor/sdk) with a local runtime: agents run on the server machine with cwd set to the target repo.
This server is the execution layer for a remote client IDE workflow:
- Task delegation — clients send prompts; the server runs Cursor agents in the appropriate local repo directory and returns run metadata and results.
- Scheduled jobs — cron-driven automation for recurring work (triage, hygiene, nightly reviews).
- Continuous reviews — deliver / deploy / exec review loops that keep remote clients aligned with repo state without running agents locally.
- Spec-driven development — a hosted spec editor and environment where fully qualified, detailed specifications drive an implementation harness end-to-end: implement → build → test → review (
deployoptional). The harness is pluggable — Cursor SDK agents today, with room for other runners (e.g. OpenCode, Hermes Agent).
Feature scope and API design are still open — see AGENTS.md for architecture notes, roadmap, and conventions.
Designed for a home lab host — not cloud-first. Typical targets:
| Target | Notes |
|---|---|
| Docker / Docker Compose | Primary packaging path; see docs/docker.md for build/up, env, and repos volume |
| Umbrel | Root Compose stack or App Store manifest — docs/umbrel.md · docs/docker.md |
| Bare metal / VM | npm run dev or npm start on any Linux box with Node 20+ |
Network access via Tailscale: recommend HOST=0.0.0.0 (bare-metal and Compose) so the published port is reachable on all interfaces. From a laptop on the same tailnet, call:
http://<host-tailscale-ip-or-MagicDNS>:<PORT>
Example health check: curl http://100.x.y.z:3000/health (or your MagicDNS name). Public internet exposure is not assumed or required — Tailscale VPN is enough. No special client setup beyond Tailscale + that URL. Details: docs/docker.md.
Living detail: .agents/specs/index.PRD.
| Phase | Focus | Status |
|---|---|---|
| Homelab-ready | Docker Compose, Umbrel manifest, Tailscale docs, client auth (SERVER_API_KEY / TENANTS), repo validation |
Landed |
| API depth | Async tasks (202 + poll), run history, SSE + WebSocket streaming, event gateway, MCP merge, spec-to-pr* agent roles, scheduled review jobs (opt-in) |
Landed |
| Spec harness | Qualified spec schema, stage orchestration, spec editor UI, pluggable runners | MVP landed |
| Runners | Cursor SDK (default), Hermes (hermes), OpenCode (opencode) |
Registered — CLIs must be installed |
| Ops UI | Homelab Kanban board (32→34); agent prompt widget (35); spec-editor aspirational UI (36); root dashboard shell (40 — GET /, /settings); board projects CRUD (39); unified app shell with route-rendered views (42) |
Landed |
| Product website | GitHub Pages website (41) |
Landed |
Caveats (honest gaps): Hermes/OpenCode adapters require external CLIs on PATH. Scheduled review jobs are off by default — set SCHEDULED_REVIEW_JOBS=true to register cron handlers. Inbox (not active): richer MCP diagnostics (only if new gaps). See AGENTS.md and index.PRD.
The spec harness is the flagship long-term feature: authors write complete, machine-actionable specs in a served environment; the server executes them through specialized stage agents (implement → build → test → review; deploy optional) with full traceability from spec item to review outcome.
Homelab-ready API with spec harness MVP. Implemented today:
GET /— ops console: one app shell (left menu, compact top bar, single main container) with a login gate that collects the API key once; the dashboard view lands hereGET /health— livenessGET /agents— task role allowlist (default,planner,implementer,plan+implementer,spec-to-pr,spec-to-pr-lite)GET/PUT /settings— host-level preference store (API key auth; SQLiteapp_settings)GET /ui/spec-editor— hosted Markdown spec editor (AC builder, stage designer, dependency graph; validate / save / Save & Run)GET /ui/board— Kanban ops UI (cards, lanes, Start/Pause/Finish)GET /ui/prompt— agent prompt widget (submit / stream / query tasks)GET /ui/projects— board repository CRUD (create / edit / delete, soft-blocked while cards reference the repo)GET /ui/config— host preference editor (default agent, harness runner, UI theme/density, board lane)GET /ui/app.css,GET /ui/app.js— shared design tokens and shell script consumed by every viewPOST /tasks— async task execution (defaultasync: true→202+taskId;async: falsefor sync wait)GET /tasks,GET /tasks/:id— list / fetch task history (persisted underREPOS_ROOT)GET /tasks/:id/stream— SSE status and log output while a task runsGET /tasks/:id/ws— WebSocket stream with the samestatus,output, anddoneevents as SSEPOST /events— event gateway (Hermes/Umbrel/IDE → async task)GET /jobs— registered cron jobs + review execution historyPOST /specs/validate,GET/PUT /repos/:repo/specs[/:file]— qualified spec IOPOST /harness/runs— stage pipeline (implement → build → test → review); runners:cursor-local,cursor-sdk,hermes,opencode- Client auth —
SERVER_API_KEYand/orTENANTSJSON;X-API-KeyorAuthorization: Bearer(disabled when neither is set) - Repo validation — exist + git working tree checks before agent start
- Docker Compose packaging + Umbrel App Store manifest (
deploy/umbrel/) + Tailscale bind/client access docs - Scheduled review jobs —
pr-diff-reviewandrepo-hygiene-checkwhenSCHEDULED_REVIEW_JOBS=true(default off)
- Node.js 20+
- Cursor API key (
CURSOR_API_KEY) - Local git clones under
REPOS_ROOT(default:./repos/<repo-name>)
npm install
cp .env.example .env
# Edit .env — set CURSOR_API_KEY and REPOS_ROOTPlace repositories the server should operate on:
repos/
my-app/ # git clone
other-repo/
npm run dev{
"agents": ["default", "planner", "implementer", "plan+implementer"],
"default": "default",
"aliases": { "generic": "default" }
}{ "status": "ok" }Interactive Markdown spec editor (no auth on the page). Lists/opens specs under a repo, live-validates via POST /specs/validate, saves to {repo}/.agents/specs/, and Save & Run dispatches POST /harness/runs. Structured panels (AC builder, stage designer, dependency graph) sync bidirectionally with the Markdown source. When SERVER_API_KEY is set, enter it in the page so API calls send X-API-Key.
Run a prompt against a repo by name (relative to REPOS_ROOT). Async by default (async: true → 202 with taskId; poll GET /tasks/:id or stream GET /tasks/:id/stream). Pass async: false for synchronous wait (legacy behavior).
Request
{
"prompt": "Review uncommitted changes for security issues",
"repo": "my-app",
"model": "composer-2",
"agent": "default",
"async": true,
"webhookUrl": "https://example.com/hook",
"source": "api"
}| Field | Required | Notes |
|---|---|---|
prompt |
yes | Task text |
repo |
yes | Folder name under REPOS_ROOT |
model |
no | Override CURSOR_MODEL |
agent |
no | Role: default (generic), planner, implementer, plan+implementer. Unknown / missing → default. Alias: generic → default. |
async |
no | Default true (202 + background run). false = sync wait. |
webhookUrl |
no | POST completion payload when task finishes |
source |
no | ide | hermes | umbrel | api (default api) |
mcpServers |
no | Per-task MCP server overrides (merged with global/repo config) |
Auth: When SERVER_API_KEY or TENANTS is configured, send X-API-Key: <key> or Authorization: Bearer <key>.
Agents
agent |
Behavior |
|---|---|
default |
Single run with the prompt as-is (generic) |
planner |
Plan only (no implement instruction) |
implementer |
Implement-focused single run |
plan+implementer |
Plan phase, then implement using that plan |
Response 202 Accepted (async default)
{
"taskId": "task_...",
"status": "pending",
"repo": "my-app",
"agent": "default",
"createdAt": "2026-07-25T..."
}Response 200 OK (when async: false)
{
"agent": "default",
"status": "finished",
"durationMs": 12345,
"run": {
"agentId": "...",
"runId": "...",
"status": "finished",
"durationMs": 12345,
"model": "composer-2",
"result": "..."
},
"result": "..."
}For plan+implementer, the sync body also includes a plan phase object alongside run.
Server-Sent Events stream of status, output, and done events while a task runs.
Event gateway — same shape as task creation (event, prompt, repo, optional agent/model/webhookUrl, source). Returns 202 with taskId.
Lists registered cron jobs and recent review-job execution history.
Server-Sent Events stream for task lifecycle and live output.
Auth (when SERVER_API_KEY or TENANTS are configured): same as other protected routes — X-API-Key, Authorization: Bearer <key>, or query parameters apiKey or access_token (query form is required for browser EventSource, which cannot set custom headers).
Events
| Event | Payload | When |
|---|---|---|
status |
{ id, status, result?, error? } |
On connect and on status change |
output |
{ id, chunk } |
Worker lifecycle lines and agent run stream chunks |
done |
{ id, status } |
Task reaches completed, failed, or cancelled; connection closes |
Example (Node / curl):
curl -N -H "X-API-Key: $SERVER_API_KEY" "http://localhost:3000/tasks/task_…/stream"
# EventSource in browser:
# new EventSource(`/tasks/${taskId}/stream?apiKey=${encodeURIComponent(key)}`)WebSocket alternative to SSE for task lifecycle and live output. Same auth and event semantics as GET /tasks/:id/stream.
URL: ws://<host>:<port>/tasks/<taskId>/ws (use wss:// when TLS terminates in front of the server).
Auth (when SERVER_API_KEY or TENANTS are configured): X-API-Key, Authorization: Bearer <key>, or query apiKey / access_token (query form is required for browser WebSocket, which cannot set custom headers on the handshake).
Messages — each WebSocket frame is JSON: { "event": "<name>", "data": <payload> }
| Event | Payload | When |
|---|---|---|
status |
{ id, status, result?, error? } |
On connect and on status change |
output |
{ id, chunk } |
Worker lifecycle lines and agent run stream chunks |
done |
{ id, status } |
Task reaches completed, failed, or cancelled; server closes the socket |
Example (browser):
const ws = new WebSocket(
`ws://localhost:3000/tasks/${taskId}/ws?apiKey=${encodeURIComponent(apiKey)}`
);
ws.onmessage = (evt) => {
const { event, data } = JSON.parse(evt.data);
// same handlers as SSE: status | output | done
};| Variable | Description | Default |
|---|---|---|
CURSOR_API_KEY |
Cursor user or team service account key | — |
PORT |
HTTP listen port | 3000 |
HOST |
HTTP bind address | 0.0.0.0 |
REPOS_ROOT |
Directory containing repo clones | ./repos |
CURSOR_MODEL |
Default model for local runs | composer-2 |
SERVER_API_KEY |
Master API key for protected routes (X-API-Key or Bearer) |
— (auth disabled when unset and no TENANTS) |
TENANTS |
JSON array of { id, apiKey, allowedRepos[] } for multi-tenant scoping |
— |
MCP_CONFIG_PATH |
Global MCP servers JSON (default config/mcp.json) |
— |
HERMES_BIN |
Hermes CLI binary for hermes runner |
hermes |
OPENCODE_BIN |
OpenCode CLI binary for opencode runner |
opencode |
OPENCODE_API_KEY |
OpenCode API key (CI / agentic-code-reviewers) | — |
SCHEDULED_REVIEW_JOBS |
Register pr-diff-review and repo-hygiene-check cron jobs at startup |
false |
SCHEDULED_REVIEW_RESUME_AGENT_ID |
Optional Cursor agent id for Agent.resume on scheduled pr-diff-review |
— |
| Command | Description |
|---|---|
npm run dev |
Start with hot reload (tsx watch) |
npm run build |
Compile TypeScript to dist/ |
npm start |
Run compiled output |
npm run typecheck |
Type-check without emit |
npm run deploy:pages |
Rebuild static GitHub Pages output in dist/pages/ |
npm run test:pages |
Rebuild and verify GitHub Pages output |
Static website source lives in website/. Run npm run deploy:pages to
rebuild its deployment output in dist/pages/; run npm run test:pages to verify that
output locally.
The Pages deployment workflow runs when a GitHub
Release is published. It checks that the release tag commit is reachable from master, runs
the repository-owned build script, uploads dist/pages/, then deploys the artifact with
GitHub's Pages deployment action. Configure the repository's Pages publishing source as
GitHub Actions before publishing a release. The workflow, not browser-side JavaScript,
performs the deployment and fails before deployment when tag validation, site build, or
output verification fails.
This repo consumes jpolvora/agentic-code-reviewers via the portable release/run.sh runner (no submodule required).
| Workflow | File | Role |
|---|---|---|
| Agentic Code Review | .github/workflows/code-review.yml |
On PR: review with opencode / opencode-go/deepseek-v4-flash; fail if open bot threads remain |
GitHub Actions secrets (repo Settings → Secrets):
| Secret | Required for | Notes |
|---|---|---|
OPENCODE_API_KEY |
review | OpenCode Go; run.sh installs CLI + writes auth.json in CI |
Thread helpers: .agents/skills/ws-fix-pr/ and ws-github-provider/scripts/ (e.g. fetch_threads.cjs). Upstream docs: workflows.md.
Local dry-run:
curl -fsSL https://raw.githubusercontent.com/jpolvora/agentic-code-reviewers/release/run.sh | bash -s -- \
--dry-run --gh --engine opencode --model opencode-go/deepseek-v4-flashPrivate — see repository owner.