An agent orchestrator that watches a GitHub repo and dispatches issues and pull requests to Claude-powered agents. It plans issues, implements approved work, runs CodeRabbit review, opens PRs, and self-handles CI failures, merge conflicts, and review comments on its own PRs — leaving plan approval and PR merge as the two gates for a human.
It is dogfooded: it runs against its own repository, with the GitHub bot account trixy.
- Python 3.12+
- uv
- GitHub CLI (
gh) — installed and authenticated - Claude Code CLI (
claude) — installed and authenticated
uv pip install -e .This installs the loony-dev console script, a command group with five
subcommands: worker, supervisor, web, setup, and hook. (There is no
bare loony-dev command.)
Run a worker from within a git repository that has a GitHub remote:
loony-dev workerThe worker auto-detects the repo from your git remote and the bot name from gh
auth, then polls every 60 seconds. Two steps are intentionally left to a human:
approving a posted plan (moving an issue to ready-for-development) and
merging a PR.
Runs the orchestrator loop for a single repository. Each pipeline — one logical work-thread per issue (or per externally-opened PR) — runs in its own git worktree, retained across all of its phases. A worker can process several pipelines concurrently; GitHub label state prevents two workers from taking the same item.
Key options:
| Option | Default | Purpose |
|---|---|---|
--repo owner/repo |
detected from git remote | Repository to watch |
--interval SECONDS |
60 |
Polling interval |
--max-concurrent-tasks N |
3 |
Pipelines worked at once (each in its own worktree) |
--work-dir PATH |
. |
Repo checkout to operate on |
--bot-name NAME |
detected from gh auth |
Bot username for watermark detection |
--allowed-users USER |
— | Always-permitted triggerers (repeatable) |
--min-role triage|write|admin |
triage |
Minimum collaborator role to trigger a run |
--stuck-threshold-hours H |
12 |
When an in-progress item is reset as stuck |
--skip-ci-checks NAME |
— | CI check names to ignore (repeatable) |
--repeated-failure-threshold N |
2 |
Identical failures before marking in-error |
--log-file PATH / -v |
— | File logging / DEBUG logging |
Discovers every repository the authenticated gh user can reach and runs a
worker for each in parallel under <base-dir>/<owner>/<repo>, restarting
crashed workers with exponential backoff. It also launches one
claude --remote-control session per repo (see Remote control).
loony-dev supervisor --base-dir ./workspaceKey options: --base-dir (checkouts + logs root), --interval (health-check
cadence, default 15s), --refresh-interval (repo re-discovery, default 1800s),
--include / --exclude (glob filters on owner/repo, repeatable),
--no-remote-control, --accept-invites-from USER (auto-accept repo invites,
repeatable). Arguments after -- are forwarded to every worker:
loony-dev supervisor --base-dir ./workspace -- --interval 30 --min-role writeA read-only web dashboard for monitoring the supervisor and workers in the
browser. It runs as a separate process and derives all state from the on-disk
layout under <base-dir>/.logs — it shares no memory with workers.
loony-dev web --base-dir ./workspace --port 5338It binds to 127.0.0.1 by default; tunnel in (e.g. SSH port-forward) to reach it
remotely, or use --host to bind another address. Warning: the dashboard
exposes mutating endpoints (write skills/commands, kill processes, attach to and
steer a task's Claude session) and has no auth — only bind to a non-loopback
address on a trusted network.
Worker, worktree, and session tables refresh on an interval. Clicking a worker
repo opens a live log stream over Server-Sent Events
(/api/logs/{owner}/{repo}/stream), which emits the recent backlog and then
pushes new lines as they are written. Clicking a session opens a live xterm.js
view of that task's Claude session and (between turns) lets you type into it —
see ClaudeSession below.
Informational, backward-compatibility only. loony-dev no longer installs hooks
into ~/.claude/settings.json; lifecycle hooks are passed to each managed
session via claude --settings, so they never affect your own claude runs.
This command just prints the hook command that will be used.
Internal — the executable Claude Code invokes for each lifecycle hook event. Not meant to be run by hand.
When the supervisor launches a claude --remote-control session per repo, it
scans that session's output for the claude.ai join URL and writes it (with the
live PID) to a per-repo connection file. The dashboard surfaces the join link /
QR code via /api/sessions, giving you a single relay per repo to join from a
phone or another machine. Pass --no-remote-control to skip this in
environments where the relay isn't reachable or you don't want a per-repo
claude session running.
Issues move through GitHub labels:
ready-for-planning→ the planning agent posts a plan as an issue comment and waits for a human to approve. New comments trigger a re-plan.ready-for-development→ the coding agent implements: code → CodeRabbit review → commit/push → open PR. It swaps the label forin-progresswhile working.in-progress→ bot actively working (auto-reset if stuck past--stuck-threshold-hours).in-error→ set after repeated identical failures; stops and requires a human.
The bot also self-handles CI failures, merge conflicts, and post-PR review comments on its own PRs. All durable state lives on GitHub — there are no local state files for issue/PR progress.
Each tick, the worker enumerates pipelines — one logical work-thread per
branch (issue-N, or pr-P for an externally-opened PR). Each pipeline returns
its single highest-priority actionable task via a pure read of GitHub + git
state, walking the priority ladder:
stuck → conflict → CI failure → PR review → planning → implementation
The scheduler then arbitrates across pipelines (global priority, the
--max-concurrent-tasks cap, in-flight dedupe) and dispatches the chosen task in
that pipeline's worktree. A pipeline's worktree is created on its first task and
retained across all subsequent phases of the issue — so consecutive phases
see the same on-disk state, and you can cd into it to inspect what the bot did
between phases. It is reclaimed only when the work reaches a terminal GitHub
state (PR merged/closed, or issue closed with no PR). See
CLAUDE.md for the full design.
Both the planning and coding agents drive Claude non-interactively via
claude -p — one subprocess per turn, prompt on stdin, context carried across
turns with --resume <session-id>. Agent prompts are packaged as Claude Code
slash commands under loony_dev/commands/*.md and invoked as
/<command> <context.json> rather than inline text.
loony_dev/
├── cli.py # Click command group (worker / supervisor / web / setup / hook)
├── orchestrator.py # Per-repo worker loop: discover → schedule → dispatch
├── supervisor.py # Multi-repo: worker-per-repo + remote-control relay
├── pipeline.py # Pipeline discovery + next_task priority ladder
├── pipeline_session.py # Per-pipeline reusable worktree + session id (#198)
├── pipeline_lease.py # Cross-process per-pipeline lock (bot vs. drive, #199)
├── git.py # GitRepo: branch + worktree lifecycle
├── coderabbit.py # Wraps `coderabbit review --agent`
├── session_registry.py # On-disk session contract (workers + dashboard)
├── models.py # Data classes (Issue, PullRequest, Comment, TaskResult)
├── agents/
│ ├── planning.py # Planning agent (claude -p)
│ ├── coding.py # Coding agent (claude -p via a thin _CliSession)
│ ├── claude_quota.py # Shared CLI invocation + quota handling
│ ├── claude_session.py # Persistent PTY session (dashboard observe/steer only)
│ ├── session_bridge.py # Framed wire protocol + per-connection mic state
│ └── session_hooks.py # Per-session lifecycle hooks via `claude --settings`
├── commands/ # Canonical slash-command markdown (installed per repo)
├── github/ # GitHub API wrappers (REST + GraphQL via gh)
├── tasks/ # One class per task type (planning, issue, pr_review, …)
└── web/ # Read-only FastAPI dashboard (SSE + htmx/Alpine/xterm.js)
ClaudeSession (agents/claude_session.py) is a persistent PTY-backed Claude
session. It no longer runs agent turns (those go through claude -p); it is
retained solely for the dashboard's live observe/steer bridge. Its input is a
single "mic": the bot holds it for the duration of a turn, and between turns a
human watching the dashboard owns the input. Mid-turn, a lone ESC interrupts the
turn (without killing the process) and any other keystroke is refused, so
operator input can't corrupt a turn in flight.
Three long-running process kinds coordinate through the filesystem only — no IPC between them except the per-session Unix sockets under the session registry:
- worker — orchestrator loop for one repo (
loony-dev worker). - supervisor — runs a worker per accessible repo and the remote-control relay
(
loony-dev supervisor). - web — the read-only dashboard, reading
<base-dir>/.logs/...(loony-dev web).
The on-disk session registry at
<base-dir>/.logs/<owner>/<repo>/sessions/<task-slug>/ (session.json,
attach.sock, injections/) is a stable contract both workers and the dashboard
touch.
To run loony-dev in a mode that automatically pulls upstream changes and restarts, use gitmon:
gitmon uv run loony-dev supervisor --base-dir ./workspacegitmon starts the supervisor immediately, then polls git fetch every 30
seconds. When new commits appear upstream, it runs git pull and restarts the
supervisor.
Running loony-dev on its own source repo:
cd ~/LoonyBin/loony-dev
gitmon -i 60 uv run loony-dev supervisor --base-dir ./workspaceTopology:
~/LoonyBin/loony-dev/ ← Running copy (monitored by gitmon, always on main)
~/LoonyBin/loony-dev/workspace/ ← Worker clones (git-ignored, invisible to outer repo)
How it stays safe:
- gitmon only restarts the supervisor process — gitmon itself remains alive through bad deployments.
- If a merged PR crashes the supervisor, gitmon waits for the next commit, pulls the fix, and restarts automatically — fully self-recovering.
- Worker clones live under
workspace/, which is in.gitignore, so they never dirty the outer repo's working tree.