Add Gitea client and git operations (B2) — step 6a, part 1 #14

Merged
dev-agent merged 2 commits from gitea-client into main 2026-09-07 20:35:18 +00:00
Collaborator

The standalone half of the Gitea layer. I split it this way because the orchestrator wiring (PR_OPEN, opening a PR after a milestone) touches control.py, which #12 also changes — this part conflicts with nothing and can land independently.

  • gitea.py — GiteaClient over httpx: open PRs, list and post issue comments, close PRs. Polling rather than webhooks per B2, so the VM stays outbound-only, and intake is structured JSON.
  • find_pull_request is the A3 payoff. It's the external-truth query that makes opening a PR idempotent across a crash — and it deliberately checks closed PRs too, since a crash after opening must not be answered with "nothing is open, so open another".
  • pull_request_reconciler returns None when Gitea is unreachable rather than guessing, so the agent gates to the human. A delayed decision costs nothing; a duplicate PR costs cleanup.
  • git.py — branch, commit, push. The token is passed per-invocation rather than stored as a remote, so it never lands in .git/config, and anything raised is scrubbed of it. commit_all returns None when there's nothing to commit, because an empty milestone is a fact to report, not an error.
  • CI secrets wired and .env.example documented.

Verified against the real sandbox before writing any of it: push to main, push a branch, open PR, comment, read comments, close, delete branch. The live test now does that round trip on every run and cleans up after itself. I also seeded the sandbox's initial commit, since it was empty and a PR needs a base.

19 new unit tests (mocked HTTP), 109 unit + 9 integration green locally.

One setup question for you — see my message: the sandbox repo is owned by nisse-dev-testing itself, which gives the agent account admin on it. That's more privilege than production should have, and it means a self-merge test wouldn't be meaningful. Cheap to change, not blocking this PR.

🤖 Generated with Claude Code

The standalone half of the Gitea layer. I split it this way because the orchestrator wiring (`PR_OPEN`, opening a PR after a milestone) touches `control.py`, which #12 also changes — this part conflicts with nothing and can land independently. - **`gitea.py`** — `GiteaClient` over `httpx`: open PRs, list and post issue comments, close PRs. Polling rather than webhooks per B2, so the VM stays outbound-only, and intake is structured JSON. - **`find_pull_request` is the A3 payoff.** It's the external-truth query that makes opening a PR idempotent across a crash — and it deliberately checks **closed** PRs too, since a crash after opening must not be answered with "nothing is open, so open another". - **`pull_request_reconciler`** returns `None` when Gitea is unreachable rather than guessing, so the agent gates to the human. A delayed decision costs nothing; a duplicate PR costs cleanup. - **`git.py`** — branch, commit, push. The token is passed per-invocation rather than stored as a remote, so it never lands in `.git/config`, and anything raised is scrubbed of it. `commit_all` returns `None` when there's nothing to commit, because an empty milestone is a fact to report, not an error. - CI secrets wired and `.env.example` documented. **Verified against the real sandbox** before writing any of it: push to main, push a branch, open PR, comment, read comments, close, delete branch. The live test now does that round trip on every run and cleans up after itself. I also seeded the sandbox's initial commit, since it was empty and a PR needs a base. 19 new unit tests (mocked HTTP), 109 unit + 9 integration green locally. **One setup question for you** — see my message: the sandbox repo is owned by `nisse-dev-testing` itself, which gives the agent account admin on it. That's more privilege than production should have, and it means a self-merge test wouldn't be meaningful. Cheap to change, not blocking this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
dev-agent added 1 commit 2026-09-07 20:27:47 +00:00
Add Gitea client and git operations (B2) — step 6a, part 1
CI / checks (push) Successful in 16s
CI / checks (pull_request) Successful in 14s
2071e6ab6d
The standalone half of the Gitea layer; orchestrator wiring follows
once the executor loop lands, to avoid colliding with it in
control.py.

- gitea.py: GiteaClient over httpx — open PRs, list and post issue
  comments, close PRs. find_pull_request is the A3 external-truth
  query that makes opening a PR idempotent across a crash, and it
  checks closed PRs too, since a crash after opening must not be
  answered with "nothing is open, so open another"
- pull_request_reconciler: an unreachable Gitea returns None rather
  than a guess, so the agent gates to the human instead of risking a
  duplicate side effect
- git.py: branch, commit, push for the workspace. The token is passed
  per-invocation rather than stored as a remote, so it never lands in
  .git/config, and anything raised is scrubbed of it
- commit_all returns None when there is nothing to commit: an empty
  milestone is a fact to report, not an error
- Live test does the real round trip against the sandbox repo and
  cleans up after itself

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dev-agent added 1 commit 2026-09-07 20:31:33 +00:00
Page to the end when searching for an existing PR
CI / checks (push) Successful in 19s
CI / checks (pull_request) Successful in 15s
8dd5e3eb15
find_pull_request read only the newest 50 per state, so once a repo
passed that many pull requests a "not found" would mean "not in the
newest page" — and the A3 reconciler would read that as "the PR was
never opened" and open a duplicate. Exactly the side effect the
intent log exists to prevent.

It now pages until a short page proves the end, and raises rather
than answering "absent" from an incomplete search; the reconciler
turns that into unverifiable and gates to the human.

Found while wiring CI: every CI run opens a PR in the sandbox, so the
threshold would have been crossed in weeks rather than never.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dongar approved these changes 2026-09-07 20:34:36 +00:00
dev-agent merged commit 1ebd2a9988 into main 2026-09-07 20:35:18 +00:00
dev-agent deleted branch gitea-client 2026-09-07 20:35:18 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dev/nisse#14