Add PR comment intake and the review loop (B2) — step 6b
- poller.py: CommentPoller polls PR comments rather than taking webhooks, keeping the VM outbound-only (B3). On an outage it backs off exponentially, caps, and retries indefinitely while logging: polling is pure intake, so an outage delays a reaction but never fails a task or forces a transition - prime() marks the existing thread as seen without delivering it, so starting a watch does not re-address feedback already handled - Review loop: a comment in PR_OPEN enters the canonical transcript, the executor responds, and the fix is pushed to the same branch with a reply on the PR. No intent is recorded for a follow-up push — the PR already exists, so a repeated push is harmless, unlike opening a second PR - Comment authors are checked against a separate Gitea operator allowlist: a PR comment is an instruction channel into an agent that writes code, so it is operator-scoped, and a Gitea login is not a Matrix MXID (A4 per-channel allowlists) - Orchestrator now holds a lock: transport and the poller deliver on separate threads, and state transitions must not interleave - Backoff timings are injectable, so the outage path is tested in milliseconds instead of being untestable Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3117565928
commit
affdb9e8aa
6 files changed
+540
-1
No files matched your search
@@ -10,7 +10,12 @@ an idempotent transition reconciled against external truth.
|
||||
from dataclasses import dataclass
|
||||
|
||||
from nisse_dev.git import GitRepo, authenticated_url
|
||||
from nisse_dev.gitea import GiteaClient, PullRequest, pull_request_reconciler
|
||||
from nisse_dev.gitea import (
|
||||
GiteaClient,
|
||||
GiteaError,
|
||||
PullRequest,
|
||||
pull_request_reconciler,
|
||||
)
|
||||
from nisse_dev.reconcile import Reconciler
|
||||
from nisse_dev.state import StateStore
|
||||
|
||||
@@ -86,6 +91,32 @@ class MilestonePublisher:
|
||||
self._store.commit_intent(intent_id, cause=f"opened PR #{pr.number}")
|
||||
return PublishResult(pr, f"opened PR #{pr.number}: {pr.url}")
|
||||
|
||||
def comment(self, index: int, body: str) -> None:
|
||||
"""Reply on the PR. Courtesy only — the pushed commit is the real
|
||||
answer — so a failure here must not derail the review loop."""
|
||||
try:
|
||||
self._client.post_comment(self._repo, index, body)
|
||||
except GiteaError:
|
||||
pass
|
||||
|
||||
def push_followup(
|
||||
self, *, task_id: str, milestone: int, message: str
|
||||
) -> str | None:
|
||||
"""Push a fix commit onto an open milestone's branch.
|
||||
|
||||
No intent is recorded: the PR already exists, so a crash mid-push
|
||||
costs a repeated push of the same commit, which is harmless. That is
|
||||
the difference from publish(), where the side effect is a new PR.
|
||||
"""
|
||||
branch = self.branch_for(task_id, milestone)
|
||||
if self._git.current_branch() != branch:
|
||||
self._git.checkout_new_branch(branch)
|
||||
sha = self._git.commit_all(message)
|
||||
if sha is None:
|
||||
return None
|
||||
self._git.push(self._remote, branch, token=self._token)
|
||||
return sha
|
||||
|
||||
|
||||
def _pr_body(milestone: int, plan: str, service_url: str | None) -> str:
|
||||
parts = [
|
||||
|
||||
Reference in new issue
Block a user