Metadata-Version: 2.4
Name: orbit-orchestrator
Version: 0.10.0
Summary: Multi-agent orchestration for human-monitored agents across forks and pull requests
Requires-Python: >=3.9
Requires-Dist: eval-type-backport>=0.2.0; python_version < '3.10'
Requires-Dist: httpx>=0.28.0
Requires-Dist: pydantic>=2.10.0
Provides-Extra: dev
Requires-Dist: fastapi>=0.115.0; extra == 'dev'
Requires-Dist: langchain-google-genai>=2.1.0; extra == 'dev'
Requires-Dist: langgraph-checkpoint-sqlite>=2.0.0; extra == 'dev'
Requires-Dist: langgraph>=0.4.0; extra == 'dev'
Requires-Dist: pydantic-settings>=2.7.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: python-dotenv>=1.0.0; extra == 'dev'
Requires-Dist: ruff>=0.8.0; extra == 'dev'
Requires-Dist: uvicorn[standard]>=0.34.0; extra == 'dev'
Provides-Extra: server
Requires-Dist: fastapi>=0.115.0; extra == 'server'
Requires-Dist: langchain-google-genai>=2.1.0; extra == 'server'
Requires-Dist: langgraph-checkpoint-sqlite>=2.0.0; extra == 'server'
Requires-Dist: langgraph>=0.4.0; extra == 'server'
Requires-Dist: pydantic-settings>=2.7.0; extra == 'server'
Requires-Dist: python-dotenv>=1.0.0; extra == 'server'
Requires-Dist: uvicorn[standard]>=0.34.0; extra == 'server'
Description-Content-Type: text/markdown

# 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](docs/setup-guide.md) · [operators](docs/operator-guide.md)

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](docs/adrs/0001-pull-based-task-queue.md) | Agents pull tasks; the orchestrator never pushes. |
| [ADR-0002](docs/adrs/0002-integration-branch-per-issue.md) | One integration branch per issue; fork PRs target it. |
| [ADR-0003](docs/adrs/0003-projects-are-data-not-config.md) | Projects are rows, not environment variables. |
| [ADR-0004](docs/adrs/0004-governance-docs-compile-to-checklists.md) | Governance docs compile to rules; one checklist delegates *and* reviews. |
| [ADR-0005](docs/adrs/0005-gitea-quirks.md) | Gitea is the forge, and it is not GitHub. |
| [ADR-0006](docs/adrs/0006-orbit-verifies-humans-merge.md) | 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

```bash
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:

```bash
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

```bash
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
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](docs/operator-guide.md).

### 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

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. Roles are `backend`,
   `frontend`, `fullstack` (one vertical slice — held to *both* handbooks), and
   `tester`.
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](docs/adrs/0005-gitea-quirks.md)). 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](docs/adrs/0006-orbit-verifies-humans-merge.md)).
   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](docs/handbooks/backend.md) for
a worked example.

```markdown
<!-- 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

```bash
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

```bash
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`.
