Self-hosted GitHub triage bot. Drives gjc --mode rpc
as a subprocess against a per-issue git worktree, then writes back to GitHub
through a sidecar that holds the PAT.
On issues.opened in an allowlisted repo it classifies the issue, labels it,
and branches:
bug/documentation→ reproduce, fix on a fresh branch, open a PR whose body has## Repro/## Cause/## Fix/## VerificationandFixes #N.question→ one comment, suffixed with a 👎-to-keep-open prompt; if the issue author doesn't react 👎 withinROBGJC_QUESTION_AUTOCLOSE_HOURS(default 4), the issue auto-closes asstate_reason=completed. A follow-up comment or external close cancels the schedule synchronously.enhancement/proposal→ one comment, no PR.invalid/duplicate→ one brief comment.
Follow-up issue comments and PR review comments resume the same gjc session
(--continue against the persisted JSONL transcript). On orchestrator
restart, in-flight events are re-queued and resume the same way.
Two containers, one trust boundary:
- robogjc — FastAPI + sqlite event queue +
WorkerPoolrunninggjcin per-issue worktrees under/data/workspaces/. Holds the HMAC key, never the PAT. - gh-proxy — sibling on an
internal: truenetwork. HoldsGITHUB_TOKEN, verifies HMAC-signed requests from robogjc, executes REST +git push. Only egress toapi.github.com.
Flow: webhook → HMAC verify → github_events.route → sqlite events
(dedup on X-GitHub-Delivery) → WorkerPool claims under
BEGIN IMMEDIATE with an in-process _inflight set per (owner, repo, n)
→ sandbox.ensure_workspace produces a worktree on farm/<8hex>/<slug>
→ worker.run_task spawns gjc --mode rpc with cwd=worktree,
persistent session_dir, model randomly drawn from ROBGJC_MODEL (CSV).
The agent uses gjc's built-in tools (read/edit/bash/lsp, scoped to
the worktree) plus the host tools in src/host_tools.py — the
exclusive surface for GitHub writes. Every host-tool invocation is audited
into the tool_calls table with credential-redacted args and results.
Requires Docker Compose v2 and a LiteLLM-style proxy on the host that your
~/.gjc/agent/models.container.yml points at (mounted into the container as models.yml; kept under a separate filename on the host so the host gjc doesn't route through the gateway). robogjc lives inside the gajae-code
monorepo at python/robogjc/; both the docker build context and the
/work/pi bind mount default to the parent monorepo (../..). Override
PI_ROOT only if you want a different gajae-code checkout backing the build
and runtime.
Bot account needs Write on every repo in ROBGJC_REPO_ALLOWLIST. A
fine-grained PAT with Contents / Issues / Pull requests RW + Metadata R is
enough.
cp .env.example .env
$EDITOR .env
openssl rand -hex 32 # ROBGJC_GH_PROXY_HMAC_KEY
openssl rand -hex 32 # GITHUB_WEBHOOK_SECRET
bun run pi:image # build gajae-code/pi:dev (one-time / on pi change)
bun run robogjc:build && bun run robogjc:up
curl -fsS http://localhost:8080/healthzThe bundled docker-compose.yml runs in gh-proxy mode by default. To run
the orchestrator directly with the PAT in-process (host CLI, tests),
comment out ROBGJC_GH_PROXY_URL / ROBGJC_GH_PROXY_HMAC_KEY and set
GITHUB_TOKEN. The two modes are mutually exclusive (config.py
rejects a .env setting both).
Build invalidation is bounded: editing robogjc Python touches only the
runtime layer; editing pi source rebuilds gajae-code/pi:dev, which
robogjc's Dockerfile.robogjc extends via FROM ${PI_BASE}.
robogjc does not ship a tunnel. Cloudflare, smee, ngrok are all fine. The
recommended ingress rule restricts the public hostname to
/webhook/github exactly; /healthz, /events, /issues, /replay
stay localhost-only.
In Settings → Webhooks: payload URL https://…/webhook/github, content
type application/json, secret = GITHUB_WEBHOOK_SECRET, events =
Issues, Issue comments, Pull requests, Pull request reviews, Pull
request review comments. GitHub's ping should produce
POST /webhook/github 202 within a second.
See .env.example for the authoritative variable list. The shipped
docker-compose.yml uses per-service environment: allowlists rather
than env_file:, so GITHUB_TOKEN only reaches the gh-proxy container.
The container entrypoint is python -m robogjc serve. Other commands run
inside the running container:
docker compose exec robogjc robogjc triage owner/repo#123 # synthesize an issues.opened and wait
docker compose exec robogjc robogjc replay <delivery_id> # re-enqueue a stored event and wait
docker compose exec robogjc robogjc status # dump issues table
docker compose exec robogjc robogjc cleanup owner/repo#123 # force workspace removal, state=abandonedbun run robogjc:… shortcuts in the root package.json cover the common
lifecycle commands (robogjc:dev, robogjc:build, robogjc:up, robogjc:down,
robogjc:logs, robogjc:restart, robogjc:reset).
pytest -x tests/ # unit suite, no network
ROBGJC_INTEGRATION=1 pytest -x tests/test_worker_smoke.pyThe integration test spawns a real gjc --mode rpc against an
httpx.MockTransport GitHub and a local bare repo, so it needs gjc on
PATH. bun run test:py runs the unit suite.
GITHUB_TOKENlives only in the gh-proxy container. The orchestrator refuses to start if it seesGITHUB_TOKENin its own environment.- Orchestrator → gh-proxy is HMAC-SHA256 signed with a ±30s skew window and constant-time compare.
git pushinside gh-proxy usesgit -c http.extraheader=…with the token passed through an ephemeral process env var; the remote URL in.git/configstays token-free.- gh-proxy has no host port. The
robogjc_internalnetwork isinternal: true(no ingress, no egress); gh-proxy joinsdefaultonly to reachapi.github.com. - Agent subprocess env is scrubbed of
GITHUB_TOKEN/ROBGJC_GH_PROXY_HMAC_KEY/ friends viaworker._SCRUBBED_ENV_KEYS. - Webhook signatures: bad sig →
401(so GitHub stops retrying), never5xx. giterrors flow throughgit_ops.GitCommandErrorwhich redactshttps://user:pw@hosttohttps://***@hostfrom argv, stdout, stderr before raising.host_tools._auditonly records agent-supplied args.- Pre-push gates (
gh_push_branch): branch matches the workspace branch, working tree clean, every commit onorigin/<default>..HEADcarriesROBGJC_GIT_AUTHOR_NAME+ROBGJC_GIT_AUTHOR_EMAIL. - Pre-PR gates (
gh_open_pr): when the repo defines them,bun run fixruns first (any diff auto-committed asstyle: bun run fix) and thenbun check. A failingbun checkreturns to the agent asRpcCommandErrorfor iteration. gh_open_prvalidates## Repro/## Cause/## Fix/## Verificationheaders and aFixes/Closes/Resolves #Nreference before opening.
- One PR per issue. Follow-up events push amendments to the same
farm/<hex>/<slug>branch. - No PR without a recorded repro. Persona prompt requires
repro_record;mark_unable_to_reproducecloses the loop when reproduction genuinely fails. - Crash recovery. On startup,
db.reset_stuck_running()flipsrunningrows back toqueued. Existing<session_dir>/*.jsonltriggers--continue. Drain bounded byROBGJC_SHUTDOWN_DRAIN_TIMEOUT_SECONDS(25s) +ROBGJC_SHUTDOWN_KILL_TIMEOUT_SECONDS(5s); composestop_grace_period: 30scovers both. - Logs. Structured JSON on stdout, rotated to
/data/logs/robogjc.log.jsonl. - Inspection (localhost only):
GET /events?limit=N,GET /issues?limit=N,GET /healthz,GET /readyz, and the dashboard at/.
| Symptom | Check |
|---|---|
401 invalid signature |
GITHUB_WEBHOOK_SECRET mismatch with the repo webhook config. |
Container exits with PI_ROOT … missing |
/work/pi mount empty inside the container; on the host either run docker compose from python/robogjc/ so PI_ROOT defaults to ../.., or export PI_ROOT to a valid gajae-code checkout. |
git push: Authentication required |
Bot PAT lacks push, or ROBGJC_BOT_LOGIN ≠ PAT's account. |
refusing to push: commit author identity mismatch |
Some commit not authored as ROBGJC_GIT_AUTHOR_*. The error lists the offending shas; git commit --amend --reset-author --no-edit. |
refusing to push: working tree is dirty |
Uncommitted agent edits. Or just call gh_open_pr, which auto-commits bun run fix output. |
bun check failed before PR creation |
Fix the reported failure and retry gh_open_pr. |
Failed to load pi_natives |
Wrong arch / missing native. bun run pi:image then bun run robogjc:build. |
No API key found for <provider> |
~/.gjc/agent/models.container.yml mount missing or provider id mismatch with ROBGJC_MODEL. |
src/
server.py FastAPI app, /webhook/github, /events, /issues, /replay, dashboard at /
github_events.py verify_signature + route()
queue.py WorkerPool, dispatch loop, per-issue _inflight serialization
tasks.py triage_issue, handle_comment, handle_pr_conversation, handle_review, cleanup_workspace
worker.py synchronous gjc RPC driver, prompt assembly, env scrubbing
host_tools.py 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
sandbox.py clone pool + worktree lifecycle
github_client.py typed httpx client; webhook payload parsing
proxy_client.py GitHubProxyClient + HMAC signer
db.py sqlite schema + DAOs
config.py pydantic Settings; mode-exclusive PAT vs gh-proxy validation
cli.py serve / triage / replay / status / cleanup
prompts/ system_append.md + per-task kickoff templates
tests/ pytest unit suite + one ROBGJC_INTEGRATION=1 smoke test
web/ vite + solid dashboard, built into src/static/
MIT.