orbit-orchestrator (0.9.0)
Installation
pip install --index-url orbit-orchestratorAbout this package
Multi-agent orchestration for human-monitored agents across forks and pull requests
Orbit
Multi-agent orchestration for human-monitored agents, across many repos, through forks and pull requests, on Gitea.
Orbit verifies. Humans merge. It checks a PR against rules compiled from your own handbooks, comments on the lines that break them, and re-checks when you fix them. It never merges anything.
Verified end to end against a live Gitea 1.23.8: issue → plan → fork PR → rule violation caught → fix → a human merges → next agent inherits the code → tracking PR out of draft. Run it yourself:
python3 scripts/e2e_gitea.py.
Setting up? → teammates · operators
A teammate keeps whatever AI tool they already like — Claude Code, Copilot, Antigravity, Cursor, or none at all. Orbit never integrates with the tool. It prepares a git branch, hands over a prompt and the checklist that PR will be judged against, and waits for a pull request.
┌──────────────────────────────────────────┐
Gitea issue ─────▶ │ Orchestrator │
labelled │ plan → delegate → verify → await merge │
orbit:auto │ (LangGraph, suspends on interrupt) │
└────────────────┬─────────────────────────┘
│ durable task queue (SQLite)
┌───────────────────────┼───────────────────────┐
│ long-poll │ long-poll │ long-poll
┌─────┴──────┐ ┌─────┴──────┐ ┌─────┴──────┐
│ orbit- │ │ orbit- │ │ orbit- │
│ runner │ │ runner │ │ runner │
│ (Kalpesh) │ │ (Vaibhav) │ │ (Akash) │
│ claude-code│ │ Copilot │ │ manual │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ fork PR │ fork PR │ fork PR
▼ ▼ ▼
org/repo : orbit/issue-42/integration ──▶ draft PR ──▶ main
▲ ▲
└─ a HUMAN merges each one ──────┘
Nothing listens on a port on a teammate's laptop. No tunnels. Laptops may sleep.
Design, in six decisions
| ADR-0001 | Agents pull tasks; the orchestrator never pushes. |
| ADR-0002 | One integration branch per issue; fork PRs target it. |
| ADR-0003 | Projects are rows, not environment variables. |
| ADR-0004 | Governance docs compile to rules; one checklist delegates and reviews. |
| ADR-0005 | Gitea is the forge, and it is not GitHub. |
| ADR-0006 | Orbit verifies. Humans merge. |
Read those before changing anything structural. Each exists because the obvious alternative fails in a way that stays invisible until it bites.
Running the orchestrator
python3 -m pip install -e '.[server,dev]' # `server` = the orchestrator's stack
cp .env.example .env # GITEA_URL, GITEA_TOKEN, webhook secret, ORBIT_SHARED_TOKEN
orbit-server
Register each repository — there is no repo name in .env, on purpose:
curl -X POST localhost:8000/v1/projects \
-H "Authorization: Bearer $ORBIT_SHARED_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"repo_full_name":"org/repo","default_branch":"main","docs_path":"docs"}'
Orbit acts as one bot account with one PAT (write:repository, write:issue,
read:user) with write access to the upstream repos it serves — and no access to
anybody's fork. A fork PR's head is fetchable from upstream as
refs/pull/<N>/head, on Gitea just as on GitHub.
Point a Gitea webhook (issues + pull_request events) at
<orchestrator>/webhooks/gitea with the same secret. Only the orchestrator needs
to be reachable — if it is not on the same network as Gitea, that is the one thing
to put behind a tunnel.
Running a runner — what your teammates install
python3 -m pip install orbit-orchestrator # from our private Gitea registry
orbit-runner setup # a browser form; validates everything before it saves
orbit-runner run # long-polls; drops you into a prepared branch
That is the whole ask. They never clone Orbit — the base package is just httpx
pydantic, and the orchestrator's stack (LangGraph, Gemini, uvicorn) lives behind theserverextra so it never touches their laptop. No open port, no tunnel, no change to their editor.
The distribution is
orbit-orchestrator, notorbit— public PyPI already has an unrelatedorbit(a satellite-tracking library). Publish withpython3 scripts/publish.py; see the operator guide.
Adapters
| Their setup | adapter |
What happens |
|---|---|---|
| Copilot, Antigravity, Cursor GUI, vim | manual |
Writes .orbit/PROMPT.md, opens it, waits for done <summary> |
| Claude Code | claude-code |
Runs claude -p "$(cat PROMPT.md)", then a human reviews before submit |
| Cursor CLI, Aider | cursor, aider |
Same shape |
| Anything else | command |
Your own shell template with {prompt_file} |
manual is the universal fallback and works with every tool that has ever
existed, because its interface is the filesystem. The deliverable is a commit, so
git commit is the handoff — nobody pastes code into a terminal.
At the orbit> prompt: done <issue> <summary>, reject <issue> <reason> (the
brief is wrong; it goes back to the planner), or fail <issue> <reason>. The issue
number is a confirmation — with two issues open you have two terminals, and done
in the wrong one would otherwise submit the wrong task.
What the orchestrator does with an issue
- Setup. Resolve the repo to a project. Cut
orbit/issue-N/integrationfrommain. Open a draft tracking PR. Compiledocs/into a rule index, cached against the docs tree SHA. - Plan. Break the issue into ordered steps, constrained by the project's accepted ADRs. Superseded ones are dropped.
- Delegate. Select the rules that bind each step, freeze them onto the task, render them into the prompt, enqueue, and suspend.
- Review. Wait for the fork's push to actually reach upstream (Gitea copies it
asynchronously — reviewing early judges the previous diff; see
ADR-0005). Then run the frozen checklist:
commandandgrepchecks first, the model only for what is left. Publish a commit status (the gate branch protection can require) plus a PR review with a comment on each offending line, citing the handbook heading. - Hand it back. On a pass, comment "approved — merge when you are ready" and park. Orbit does not merge (ADR-0006). A push after approval revokes the green check and re-verifies — no unreviewed code lands behind a stale pass.
- Resume on merge. When a human merges, the next agent branches from the
integration branch and finds that code already in their working tree. That is
the whole context-passing mechanism: a
git merge, not a JSON blob. - Complete. Review
main...integrationagainst the union of every checklist, take the tracking PR out of draft, and comment the result on the issue. Landing it onmainis a person's call too.
A failed rule quotes itself back in the retry prompt:
RETRY 1/3. Your previous PR did not pass review.
## Rules to fix
- **BE-API-004** — Never leave print statements in a request handler.
source: `docs/handbooks/backend.md#api-conventions`
Writing governance docs
Under docs/, files whose path contains adr or decisions are ADRs; everything
else is a rulebook. See docs/handbooks/backend.md for
a worked example.
<!-- orbit:rule id=BE-API-004 roles=backend paths=api/** check=grep:!print\( -->
Never leave print statements in a request handler; use the module logger.
check=command:<cmd>— runs in a checkout; non-zero exit fails.check=grep:<regex>— regex over added lines; a leading!inverts it.check=llm(default) — judgement, costs a model call.
Unannotated prose falls back to LLM extraction with content-hashed IDs. Annotating is how you get determinism, and it is the difference between "this rule keeps failing, let's fix the doc" and a fresh paragraph of vibes every run.
ADRs need a status. Only accepted binds.
A rule whose scope nothing touched resolves to skip, never pass. A rule that
could not be evaluated resolves to error, and blocks exactly like a failure —
not being able to check something is not the same as it being fine.
Without a GOOGLE_API_KEY, compilation still yields every annotated rule, planning
falls back to a static plan, and llm rules report error rather than silently
passing.
Layout
orbit/
store.py SQLite: projects, workflows, tasks (the queue), agents, rule index
config.py installation config only — never a repo name
forge/ Gitea REST client, unified-diff parser, + an in-memory fake
governance/ docs/ -> rules + decisions; checklist selection
review/ command/grep/llm checks; status + inline-review publishing
orchestrator/ the LangGraph nodes and the graph
server/ FastAPI: webhooks, the task queue, projects
runner/ orbit-runner: claim, prepare a branch, adapters, open the PR
Tests
pytest # 163 tests, no network, no API keys
ruff check orbit tests scripts
The store, the queue, the compiler, and the graph are exercised for real; only the
forge and the LLM are doubles. FakeForge records every branch, PR, merge, commit
status, and inline comment, so tests assert on what the orchestrator did.
Against a real Gitea
python3 scripts/e2e_gitea.py --preflight # check creds, create nothing
python3 scripts/e2e_gitea.py # the full run
python3 scripts/probe_gitea.py # dump Gitea's real response shapes
e2e_gitea.py creates a throwaway org, repo, and three fork orgs; drives one issue
to a merged integration branch; and deletes everything. It refuses to act on any
name it did not mint (orbit-(e2e|up|be|fe|qa)-<timestamp>), and prints the
blast radius before it starts.
It is not a smoke test — it deliberately opens a PR that violates a handbook rule and asserts Orbit catches it, quotes the rule back, refuses the merge, accepts the fix, merges it, and hands the code to the next agent. Four production bugs were found this way, each of which the unit tests happily passed:
- A runner adopting Orbit's own tracking PR as its own (Gitea ignores
headfilters). - Reviewing a stale diff, because a fork's push reaches upstream asynchronously — which can pass a broken change.
- Blaming the agent for a merge failure that was the forge running out of threads.
greprules silently passing, because Gitea's/filesendpoint has nopatch.