diff --git a/docs/prds/prd-new-fold-orchestrator-subpackage.md b/docs/prds/prd-new-fold-orchestrator-subpackage.md new file mode 100644 index 0000000..17ccc30 --- /dev/null +++ b/docs/prds/prd-new-fold-orchestrator-subpackage.md @@ -0,0 +1,132 @@ +# PRD prd-new: Fold bot-bottle-orchestrator into this repo + +- **Status:** Draft +- **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.