Files
bot-bottle/docs/prds/prd-new-fold-orchestrator-subpackage.md
T
didericis-claude df1f0e8f70
lint / lint (push) Successful in 2m4s
test / unit (pull_request) Successful in 56s
test / integration (pull_request) Successful in 22s
test / coverage (pull_request) Failing after 1m7s
docs: mark fold-orchestrator PRD as Active
2026-07-01 17:18:38 +00:00

4.7 KiB

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.pyScopedForge

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.pyGiteaClient, 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.pyForgeState, 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.