orbit-orchestrator (0.3.0)

Published 2026-07-12 11:50:42 +00:00 by ravitejas

Installation

pip install --index-url  orbit-orchestrator

About 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 init      # writes ~/.orbit/config.json
orbit-runner doctor    # checks remotes, token, connectivity, adapter
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 the server extra so it never touches their laptop. No open port, no tunnel, no change to their editor.

The distribution is orbit-orchestrator, not orbit — public PyPI already has an unrelated orbit (a satellite-tracking library). Publish with python3 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 <summary>, reject <reason> (the generated prompt is wrong; it goes back to the planner), or fail <reason>.

What the orchestrator does with an issue

  1. Setup. Resolve the repo to a project. Cut orbit/issue-N/integration from main. Open a draft tracking PR. Compile docs/ into a rule index, cached against the docs tree SHA.
  2. Plan. Break the issue into ordered steps, constrained by the project's accepted ADRs. Superseded ones are dropped.
  3. Delegate. Select the rules that bind each step, freeze them onto the task, render them into the prompt, enqueue, and suspend.
  4. 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: command and grep checks 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.
  5. 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.
  6. 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.
  7. Complete. Review main...integration against the union of every checklist, take the tracking PR out of draft, and comment the result on the issue. Landing it on main is 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:

  1. A runner adopting Orbit's own tracking PR as its own (Gitea ignores head filters).
  2. Reviewing a stale diff, because a fork's push reaches upstream asynchronously — which can pass a broken change.
  3. Blaming the agent for a merge failure that was the forge running out of threads.
  4. grep rules silently passing, because Gitea's /files endpoint has no patch.

Requirements

Requires Python: >=3.9
Details
PyPI
2026-07-12 11:50:42 +00:00
1
73 KiB
Assets (1)
Versions (8) View all
0.10.0 2026-07-13
0.9.0 2026-07-13
0.8.0 2026-07-13
0.7.0 2026-07-13
0.6.0 2026-07-13