Forge native integration: PRD + forge library layer #318

Open
didericis-claude wants to merge 8 commits from forge-native-integration into main
Collaborator

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.pyForge ABC + ScopedForge, enforcing
    read-anywhere / write-scoped (ForgeScopeError on out-of-scope writes).
  • contrib/gitea/client.pyGiteaClient (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.pyForgeState + atomic
    read/write/delete/all under ~/.bot-bottle/forge/<owner>/<repo>/.
  • contrib/gitea/provenance.pybuild_provenance_footer, the
    collapsed markdown audit footer (watchdog / gitleaks / egress rows).
  • cli/resume.pyresume --headless --prompt, reusing the shipped
    assume_yes + headless_prompt launch core (the new half of chunk 1).

47 new unit tests; pylint 9.98/10, pyright clean.

Deferred (correctly out of scope here)

  • Forge sidecar (chunk 4) and orchestrate command (chunk 6):
    their only consumer is the separate orchestrator; untestable as
    standalone shells in this repo.
  • forge_env plumbing: no in-repo consumer until the sidecar exists.

Merge rule(s)

Requires human review and merge — no auto-merge.

Closes #317. [PRD](https://gitea.dideric.is/didericis/bot-bottle/src/branch/forge-native-integration/docs/prds/prd-new-forge-native-integration.md) ## 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`** — `Forge` ABC + `ScopedForge`, enforcing read-anywhere / write-scoped (`ForgeScopeError` on 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 shipped `assume_yes` + `headless_prompt` launch core (the new half of chunk 1). 47 new unit tests; pylint 9.98/10, pyright clean. ## Deferred (correctly out of scope here) - **Forge sidecar** (chunk 4) and **`orchestrate` command** (chunk 6): their only consumer is the separate orchestrator; untestable as standalone shells in this repo. - **`forge_env` plumbing**: no in-repo consumer until the sidecar exists. ## Merge rule(s) Requires human review and merge — no auto-merge.
didericis-claude added 1 commit 2026-06-29 12:10:59 -04:00
didericis-claude added 1 commit 2026-06-29 12:39:12 -04:00
didericis added 1 commit 2026-06-30 17:39:41 -04:00
Flip the forge-native-integration PRD from option 2 (agent calls the
Gitea API directly via cred-proxy; done signal parsed from comments) to
option 3 per issue #317 comment 2715: a forge sidecar backed by a Forge
abstract class.

- signal_done(status, summary) replaces comment-parsing as the done signal
- semantic audit trail from the sidecar feeds provenance directly
- read-anywhere / write-scoped enforcement, tighter than repo-wide API keys
- forge-agnostic agent prompts and sidecar protocol
- DeployKeyProvisioner subsumption deferred; share the HTTP client only

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WL77TgFxKbs3cidGMG9dz7
didericis added 1 commit 2026-06-30 17:47:02 -04:00
#315 already merged `start --headless` (assume_yes on _launch_bottle +
AgentProvider.headless_prompt). The PRD's proposed start_headless /
attach_agent_headless helpers were redundant with it, and the latter
diverged by hand-rolling --no-interactive/-p instead of using the
headless_prompt provider abstraction. Drop them.

Scope the remaining headless work to what's actually new: a forge_env
hook threaded into the existing _launch_bottle core, and a `resume
--headless` path (resume has no non-interactive entry point today).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WL77TgFxKbs3cidGMG9dz7
didericis added 2 commits 2026-06-30 19:39:57 -04:00
feat(forge): forge library layer for native integration (PRD chunks 1-3, 5)
lint / lint (push) Failing after 2m9s
test / unit (pull_request) Successful in 58s
test / integration (pull_request) Successful in 21s
test / coverage (pull_request) Successful in 1m23s
a229a22d54
Implements the bot-bottle side of the forge-native PRD that is
self-contained in this repo (the forge sidecar and orchestrate command
belong to the separate bot-bottle-orchestrator, a PRD non-goal):

- contrib/forge/base.py: Forge ABC + ScopedForge enforcing the
  read-anywhere / write-scoped model (writes rejected outside the
  assigned issue/PRs via ForgeScopeError).
- 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 dataclass + atomic
  read/write/delete/all under ~/.bot-bottle/forge/<owner>/<repo>/.
- contrib/gitea/provenance.py: build_provenance_footer — collapsed
  markdown audit footer; watchdog/gitleaks/egress rendering.
- cli/resume.py: `resume --headless --prompt` reusing the shipped
  assume_yes + headless_prompt launch core (the new half of chunk 1).

47 new unit tests; pylint 9.98/10, pyright clean. Forge sidecar (chunk
4), orchestrate command (chunk 6), and forge_env plumbing are deferred:
their only consumer is the separate orchestrator and they are untestable
in isolation here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WL77TgFxKbs3cidGMG9dz7
didericis-claude changed title from PRD: Forge native integration to Forge native integration: PRD + forge library layer 2026-06-30 22:41:17 -04:00
didericis added 1 commit 2026-07-01 07:59:08 -04:00
fix(tests): resolve pyright strict errors in forge test helpers
lint / lint (push) Successful in 2m18s
test / unit (pull_request) Successful in 1m1s
test / integration (pull_request) Successful in 22s
test / coverage (pull_request) Successful in 1m19s
f211ece6bf
CI runs `pyright .` over the whole repo including tests; the earlier
run only checked the source paths. The test helpers used `**over`
dict-splat into typed constructors, which pyright strict rejects.

- forge_state: build a typed ForgeState base and dataclasses.replace(**over)
- provenance: explicit typed keyword params instead of a **over dict
- resume: _launch_kwargs returns dict[str, Any] (copy call_args.kwargs)
- forge_base: assert PermissionError in __mro__ (avoids always-true issubclass)
- client: annotate _resp body param; type: ignore the mock __enter__ lambda

pyright . now 0 errors; 47 tests still pass; pylint 9.97/10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WL77TgFxKbs3cidGMG9dz7
didericis reviewed 2026-07-01 08:02:43 -04:00
@@ -0,0 +27,4 @@
@dataclass(frozen=True)
class Issue:
"""A forge issue or PR (forges model PRs as issues with the same
number)."""
Owner

There should also be a “PullRequest” dataclass/issues and PullRequests should not use the same object

There should also be a “PullRequest” dataclass/issues and PullRequests should not use the same object
didericis reviewed 2026-07-01 08:03:43 -04:00
@@ -0,0 +53,4 @@
@abc.abstractmethod
def read_issue(self, number: int) -> Issue:
"""Read an issue or PR body (read-anywhere)."""
Owner

Should be a separate “read_pr” method

Should be a separate “read_pr” method
didericis marked this conversation as resolved
didericis reviewed 2026-07-01 08:11:26 -04:00
@@ -0,0 +5,4 @@
freeze / rehydrate loop, and run the watchdog. State lives on disk and
survives orchestrator restarts:
~/.bot-bottle/forge/<owner>/<repo>/issue-<n>.json
Owner

We should introduce sqlite and start writing state in a local db now. We should do it the following way:

  1. db lives in the home ~/.bot-bottle folder
  2. there’s a thin api for performing crud against the db so we can swap out different storage locations
We should introduce sqlite and start writing state in a local db now. We should do it the following way: 1. db lives in the home ~/.bot-bottle folder 2. there’s a thin api for performing crud against the db so we can swap out different storage locations
didericis reviewed 2026-07-01 08:12:01 -04:00
@@ -0,0 +1,103 @@
"""Provenance footer (PRD forge-native-integration, chunk 5).
Every orchestrator-posted comment ends with this footer non-optional
and not configurable off. It renders the run's audit trail (agent,
Owner

Get rid of the provenance footer

Get rid of the provenance footer
didericis reviewed 2026-07-01 08:13:44 -04:00
@@ -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 product
Owner

Reword this: we will have a provenance api, but we won’t surface it in the pr

Reword this: we will have a provenance api, but we won’t surface it in the pr
didericis added 1 commit 2026-07-01 08:37:30 -04:00
refactor(forge): address PR #318 review — PR/Issue split, sqlite state, drop footer
lint / lint (push) Successful in 1m59s
test / unit (pull_request) Successful in 48s
test / integration (pull_request) Successful in 17s
test / coverage (pull_request) Successful in 58s
42004d37fd
Addresses the five review comments on PR #318:

- Split PullRequest from Issue and add a dedicated read_pr method on
  Forge/ScopedForge/GiteaForge (a PR carries merge state an issue does
  not); is_pr_open now derives from read_pr.
- Replace the JSON-file forge state with a thin swappable CRUD interface
  (ForgeStateStore) backed by SQLite (SqliteForgeStateStore) at
  ~/.bot-bottle/bot-bottle.db.
- Remove the provenance footer (provenance.py + its test): a mutable,
  unsigned PR comment is not an audit record.
- Reword the PRD: provenance is exposed via an API, not surfaced in the
  PR; document the Issue/PullRequest split and the SQLite store.

pyright clean (whole repo), pylint 10/10, 38 forge/resume unit tests pass;
no remaining refs to the removed provenance module or old JSON state API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WL77TgFxKbs3cidGMG9dz7
Author
Collaborator

Addressed the review in commit 42004d3:

  • PullRequest dataclass + read_prIssue and PullRequest are now separate frozen dataclasses (a PR carries merged), read through separate read_issue / read_pr methods on Forge / ScopedForge / GiteaForge. is_pr_open now derives from read_pr.
  • SQLite state — replaced the JSON-file store with a thin swappable CRUD interface ForgeStateStore (upsert/get/delete/all), backed by SqliteForgeStateStore at ~/.bot-bottle/bot-bottle.db. Storage location/engine is swappable behind the interface.
  • Provenance footer removed — deleted provenance.py and its test.
  • PRD reworded — provenance is now described as a provenance API (not surfaced in the PR); also documents the Issue/PullRequest split and the SQLite store.

pyright clean (whole repo), pylint 10/10, forge/resume unit tests pass.

Addressed the review in commit 42004d3: - **`PullRequest` dataclass + `read_pr`** — `Issue` and `PullRequest` are now separate frozen dataclasses (a PR carries `merged`), read through separate `read_issue` / `read_pr` methods on `Forge` / `ScopedForge` / `GiteaForge`. `is_pr_open` now derives from `read_pr`. - **SQLite state** — replaced the JSON-file store with a thin swappable CRUD interface `ForgeStateStore` (upsert/get/delete/all), backed by `SqliteForgeStateStore` at `~/.bot-bottle/bot-bottle.db`. Storage location/engine is swappable behind the interface. - **Provenance footer removed** — deleted `provenance.py` and its test. - **PRD reworded** — provenance is now described as a provenance **API** (not surfaced in the PR); also documents the Issue/PullRequest split and the SQLite store. pyright clean (whole repo), pylint 10/10, forge/resume unit tests pass.
Some checks are pending
lint / lint (push) Successful in 1m59s
test / unit (pull_request) Successful in 48s
test / integration (pull_request) Successful in 17s
test / coverage (pull_request) Successful in 58s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin forge-native-integration:forge-native-integration
git checkout forge-native-integration
Sign in to join this conversation.