# PRD prd-new: Fold bot-bottle-orchestrator into this repo - **Status:** Active - **Author:** didericis - **Created:** 2026-07-01 - **Issue:** #321 ## Summary Move the `bot-bottle-orchestrator` binary into `bot_bottle/orchestrator/` as a first-class subpackage. `pip install bot-bottle` gets you everything; the orchestrator's entry point becomes `python -m bot_bottle.orchestrator run`. The cross-repo CLI contract becomes an internal boundary, and the forge integration layer (`GiteaClient`, `ScopedForge`, `SqliteForgeStateStore`) is promoted to `bot_bottle/contrib/` where it belongs. ## Problem The orchestrator and bot-bottle are tightly coupled: - It always deploys on the same host. - It imports from `bot_bottle` for the forge/state layer. - Its runner shims (`start --headless`, `commit`, `resume`) map 1:1 to CLI commands in `cli.py` — a breaking CLI change silently breaks the orchestrator with no CI signal. - Two repos means two version pins, two CI pipelines, and two install steps every time the deploy environment is rebuilt. ## Goals / Success Criteria - All orchestrator modules live under `bot_bottle/orchestrator/` and the package is importable as `from bot_bottle.orchestrator import ...`. - `python -m bot_bottle.orchestrator run` starts the webhook server. - `python -m bot_bottle.orchestrator status` prints tracked runs. - The forge integration layer (`GiteaClient`, `GiteaForge`, `ScopedForge`, `ForgeState`, `SqliteForgeStateStore`) lives in `bot_bottle/contrib/` and is covered by tests in `tests/unit/orchestrator/`. - All orchestrator unit tests pass under bot-bottle's existing CI (`python -m unittest discover -s tests/unit`). - No functional change to the orchestrator's external behaviour: same HTTP surface, same webhook protocol, same env-var config, same CLI flags. ## Non-goals - Replacing `SubprocessBottleRunner` with a direct programmatic runner — the subprocess shim stays; the `BottleRunner` protocol remains the internal abstraction point. - Merging the orchestrator's SQLite DB with any other bot-bottle state store. - Archiving `bot-bottle-orchestrator` (that happens after this ships and the deploy is updated; out of scope for this PR). ## Design ### Package layout ``` bot_bottle/ orchestrator/ __init__.py __main__.py # python -m bot_bottle.orchestrator bootstrap.py # wires contrib modules → orchestrator core config.py events.py lifecycle.py model.py provenance.py runner.py sidecar.py store.py targeting.py watchdog.py webhook.py contrib/ forge/ __init__.py base.py # ScopedForge: read-anywhere / write-scoped wrapper gitea/ client.py # GiteaClient (urllib.request), GiteaForge forge_state.py # ForgeState dataclass + SqliteForgeStateStore tests/unit/orchestrator/ __init__.py _fakes.py test_config.py test_events.py test_lifecycle.py test_provenance.py test_runner.py test_sidecar.py test_store.py test_targeting.py test_watchdog.py test_webhook.py ``` ### Module moves Every `orchestrator/` source file moves verbatim into `bot_bottle/orchestrator/`. Internal imports are already relative (`from .config import Config`) so no changes are needed inside the orchestrator modules themselves. `bootstrap.py` is the only file that changes meaningfully: the lazy `bot_bottle` imports become direct relative imports (`from ..contrib.gitea.client import …`), and the `_require_bot_bottle()` guard is removed since the package is always present. ### New contrib modules **`bot_bottle/contrib/forge/base.py` — `ScopedForge`** Wraps any forge object and enforces read-anywhere / write-scoped access: reads pass through unconditionally; `post_comment` and `update_description` raise `PermissionError` for issue/PR numbers outside the assigned set. **`bot_bottle/contrib/gitea/client.py` — `GiteaClient`, `GiteaForge`** `GiteaClient` is a thin `urllib.request`-only HTTP wrapper (no new Python dependencies). `GiteaForge` composes a client and exposes the forge protocol: `is_org_member`, `read_issue`, `read_pr`, `read_comments`, `post_comment`, `update_description`. **`bot_bottle/contrib/gitea/forge_state.py` — `ForgeState`, `SqliteForgeStateStore`** `ForgeState` is a dataclass mirroring `RunRecord` field-for-field. `SqliteForgeStateStore` backs it with SQLite (stdlib `sqlite3`): a single `forge_state` table with one row per (owner, repo, issue\_number). ### Test migration All orchestrator test files move to `tests/unit/orchestrator/` with absolute imports updated from `orchestrator.X` to `bot_bottle.orchestrator.X`. The unit discovery command (`-s tests/unit`) picks them up automatically — no CI changes required.