Forge native integration: PRD + forge library layer #318
Reference in New Issue
Block a user
Delete Branch "forge-native-integration"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #317.
PRD
Design
Forge-native runs are driven by a forge sidecar (option 3): the agent calls the sidecar, which holds the forge token and makes the actual Gitea API calls. The agent never sees the credential or a forge-specific endpoint. Completion is an unambiguous
signal_done(status, summary)sidecar call relayed to the orchestrator over a queue dir — no comment-parsing. Scope is read-anywhere / write-scoped: reads for context are unrestricted; writes are limited to the assigned issue and its PRs.The orchestration loop + webhook listener live in a separate binary (
bot-bottle-orchestrator, a PRD non-goal). This PR covers bot-bottle's side of that contract.What this PR implements
The self-contained forge library layer both the sidecar and orchestrator build on (PRD chunks 1–3, 5):
contrib/forge/base.py—ForgeABC +ScopedForge, enforcing read-anywhere / write-scoped (ForgeScopeErroron out-of-scope writes).contrib/gitea/client.py—GiteaClient(stdlib-only HTTP, mirrors the deploy-key provisioner) +GiteaForge. Token held by the caller (the sidecar), not injected by cred-proxy.contrib/gitea/forge_state.py—ForgeState+ atomic read/write/delete/all under~/.bot-bottle/forge/<owner>/<repo>/.contrib/gitea/provenance.py—build_provenance_footer, the collapsed markdown audit footer (watchdog / gitleaks / egress rows).cli/resume.py—resume --headless --prompt, reusing the shippedassume_yes+headless_promptlaunch core (the new half of chunk 1).47 new unit tests; pylint 9.98/10, pyright clean.
Deferred (correctly out of scope here)
orchestratecommand (chunk 6): their only consumer is the separate orchestrator; untestable as standalone shells in this repo.forge_envplumbing: no in-repo consumer until the sidecar exists.Merge rule(s)
Requires human review and merge — no auto-merge.
PRD: Forge native integrationto Forge native integration: PRD + forge library layer@@ -0,0 +27,4 @@@dataclass(frozen=True)class Issue:"""A forge issue or PR (forges model PRs as issues with the samenumber)."""There should also be a “PullRequest” dataclass/issues and PullRequests should not use the same object
@@ -0,0 +53,4 @@@abc.abstractmethoddef read_issue(self, number: int) -> Issue:"""Read an issue or PR body (read-anywhere)."""Should be a separate “read_pr” method
@@ -0,0 +5,4 @@freeze / rehydrate loop, and run the watchdog. State lives on disk andsurvives orchestrator restarts:~/.bot-bottle/forge/<owner>/<repo>/issue-<n>.jsonWe should introduce sqlite and start writing state in a local db now. We should do it the following way:
@@ -0,0 +1,103 @@"""Provenance footer (PRD forge-native-integration, chunk 5).Every orchestrator-posted comment ends with this footer — non-optionaland not configurable off. It renders the run's audit trail (agent,Get rid of the provenance footer
@@ -0,0 +447,4 @@alongside the agent for forge-targeted runs. Tests: arg parsing, `start`delegates to `start --headless`, `resume` delegates to `resume --headless`.## Provenance as the productReword this: we will have a provenance api, but we won’t surface it in the pr
Addressed the review in commit
42004d3:PullRequestdataclass +read_pr—IssueandPullRequestare now separate frozen dataclasses (a PR carriesmerged), read through separateread_issue/read_prmethods onForge/ScopedForge/GiteaForge.is_pr_opennow derives fromread_pr.ForgeStateStore(upsert/get/delete/all), backed bySqliteForgeStateStoreat~/.bot-bottle/bot-bottle.db. Storage location/engine is swappable behind the interface.provenance.pyand its test.pyright clean (whole repo), pylint 10/10, forge/resume unit tests pass.
83cb64378cto42004d37fdAdded
bot_bottle/api.pywith the stable Python API the orchestrator needs for aProgrammaticBottleRunner:What changed:
BottleSpec.forge_env— new field carries forge sidecar env vars through to the backend. Defaults to{}for non-forge runs; the forge sidecar (chunk 4) will pick it up at launch time._launch_bottlereturns(slug, exit_code)instead of always-0int. The slug letsstart_headless()hand it back to the orchestrator; the real exit code lets the API convert non-zero toBottleError. CLI callers unpack and return just the exit code — no behaviour change for operators.bot_bottle/api.py— four public functions:start_headless(agent, *, prompt, bottles, forge_env, ...)→ slugresume_headless(slug, *, prompt, forge_env, ...)→ Nonefreeze(slug, *, backend_name)→ Nonedestroy(slug, *, backend_name)→ None (brings down compose project + removes state dir; idempotent)All four convert
Dieand non-zero exits toBottleError(message, exit_code=N).27 new unit tests in
tests/unit/test_api.py; existing headless tests updated for the new return type.forge_envis stored onBottleSpecand threaded intostart_headless/resume_headless, but not yet consumed — chunk 4 (forge sidecar) will readspec.forge_envto inject into the sidecar container at launch time.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.