Forgejo: submitted reviews continue the implementer session on its own PR #45
Labels
No labels
Agent
Ignore
Agent
In Progress
Agent
Planned
Agent
Planning
Agent
Ready
Agent
Triage
Compat
Breaking
Kind
Bug
Kind
Documentation
Kind
Enhancement
Kind
Feature
Kind
Security
Kind
Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
lucas/omp-deck#45
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Implementation: #51
Problem
A review submitted on the implementer session's own pull request should continue that session. That way revisions can be driven through normal Forgejo reviews (implementer split 3/3; splits 1/3 and 2/3 are merged in #47 and #48).
apps/server/src/forgejo/webhook.tsignores the review event types, and its header comment wrongly calls them "inline review comments".forgejo_create_pr_thread_comment(what changed plus the commit sha, or why nothing changed). It posts one PR comment only when the review body itself had feedback.Plan reference: KB
project/forgejo-integration-plan.md, review part of step 15. Design:project/forgejo-integration.md(Implementer, Reviewer).Plan
Forgejo facts (from Forgejo source:
services/webhook/notifier.goPullRequestReview,modules/webhook/type.go)X-Forgejo-Event-Typeispull_request_review_approved,pull_request_review_rejectedorpull_request_review_comment.X-Forgejo-Eventispull_request_approved,pull_request_rejectedorpull_request_comment. Pending reviews do not fire it.PullRequestPayloadwithaction: "reviewed",number,pull_request(incl.user,head.ref,head.sha,base.ref,title,body,html_url),sender= reviewer, andreview: { type, content }(content = review body). No review id.repository.permissionsin this payload is computed for the PR author, not the reviewer. On the agent's own PR it is the agent's permission, so the existingperms.push === truecheck inForgejoDispatcher.gate(dispatcher.ts~L290) proves nothing for reviews.issue_comment/ Event-Typepull_request_comment; that path is unchanged.How the merged implementer code works (read before editing)
ForgejoImplementerScope(apps/server/src/bridge/session-policy.ts~L51):{ owner, repo, number /* issue */, baseBranch }. PRs the session opened are not in the scope.InProcessBridge.mapImplementerPr(bridge/in-process.ts~L745) maps them inforgejo_sessions, andimplementerPrNumbers(scope, sessionId)(session-policy ~L231) lists them back.reuseMapped(dispatcher.ts~L406) skips the number-equality check forforgejo_implementer, so an issue or PR mapped to an implementer session already routes to it.resolveSession(~L364) createsforgejo_pull_request/forgejo_conversationsessions only. Implementer sessions are created only bystartImplementer(~L585).in-process.ts~L336-351 withkind: "issue"for implementer sessions andnumber = scope.number.Step 1: implementer scope can be started from a PR (
session-policy.ts,in-process.ts)Add an optional field to
ForgejoImplementerScope, documented:/** The pull request the session was started from (its checkout is that PR's clone); absent for sessions started for an issue. Equal to \number` when the PR names no issue. */ pull?: number;. It is optional because persistedscope_json` rows from #47/#48 lack it.implementerPrNumbers: return the mapped numbers exceptscope.number, plusscope.pullfirst when it is set and not already in the list. Soforgejo_create_pris denied and the PR line appears insystemContextfor PR-started sessions, including PR-only ones.systemContext: whenscope.pull === scope.number(PR-only), replace the title and the issue-oriented bullets with:## Unattended Forgejo implementer session (PR #${pull})You maintain pull request #${pull}, which you authored. Nobody is watching this session live: the only channel to humans is Forgejo.- Read the pull request with \forgejo_read_pr` and its conversation with `forgejo_list_pr_comments_and_reviews`; no issue is linked to it.`- Work on the pull request's branch, commit with \git_commit`, push with `git_push`. Do not open another pull request.`- Post blockers, open questions and major decisions as comments on #${pull}.Otherwise keep the current text. For all implementer sessions, append the bullet
- Reviews submitted on your pull request arrive as turns: answer line comments in their threads with \forgejo_create_pr_thread_comment`.`decideneeds no change: comments onscope.numberare allowed, and the PR is eitherscope.numberor mapped to the session.in-process.tsresume (~L341-351): forforgejo_implementerwithscope.pull !== undefined, restore withkind: "pr",number: scope.pull,baseBranch: null. Otherwise keep today's arguments.Tests in the existing session-policy test file (find it with
glob apps/server/src/bridge/session-policy*.test.ts): PR-only scope → the PR-only header, andforgejo_create_prdenied withthis session already opened pull request #<pull>. Scope withpull≠numberand no mapping →forgejo_create_prdenied. Old scope withoutpull→ unchanged behavior.Step 2: classify review deliveries (
apps/server/src/forgejo/webhook.ts)const REVIEW_EVENT_TYPES = ["pull_request_review_approved", "pull_request_review_rejected", "pull_request_review_comment"] as const.resolveEventType: without an Event-Type header, map Eventpull_request_approved→pull_request_review_approved,pull_request_rejected→pull_request_review_rejected, andpull_request_comment→pull_request_review_comment. Everything else passes through. Rewrite its doc comment and the file-header bullet (~L14-16): those are submitted reviews, not inline comments.classifyForgejoDelivery: routeREVIEW_EVENT_TYPESto a newclassifyReviewSubmitted(eventType, payload, agentUsername), modeled onclassifyReviewRequest:malformed "payload is not an object";action !== "reviewed"→ignore "unsupported action <x>".sender.login→malformed "missing sender.login"; sender equal to the agent (case-insensitive) →ignore "self-authored".repository.full_nameand the number (payload.number ?? pull_request.number) are validated with the same messages asclassifyReviewRequest.dedupeKey= sha256 ofJSON.stringify(["reviewed", payload]).event: eventType,action: "reviewed",repo,number,isPull: true,commentAuthor: sender.db/migrations/010-forgejo-triggers.sqlare documentation only.Step 3: Forgejo calls (
apps/server/src/forgejo/dispatch-api.ts)Add to
DispatchApiandcreateDispatchApi, in the style oflistComments/readIssue(never throw, returnApiResult):listReviews(owner, repo, number, signal?): Promise<ApiResult<ReviewSummary[]>>→GET pulls/{n}/reviewsviafetchAllPages. Exportedinterface ReviewSummary { id: number; author: string; state: string; commentsCount: number; submittedAt: string }, fromid,user.login,state,comments_count,submitted_at; missing fields become""/0.reviewerCanWrite(owner, repo, login, signal?): Promise<ApiResult<boolean>>:GET collaborators/{login}/permission. ok →permission∈ {write,admin,owner}.GET /repos/{o}/{r}.owner.loginequal tologin(case-insensitive) → true. OtherwiseGET /repos/{o}/{r}/teams, and for each team withpermission∈ {write,admin,owner} orunits_map["repo.code"]∈ {write,admin},GET /teams/{id}/members/{login}: 200 → true, 404 → next team. A 403/404 on the teams list (user-owned repo, or the bot can't see teams) → false. No match → false.The doc comment states why: review payloads carry the PR author's permission, and Forgejo answers the collaborator-permission endpoint only for admins.
Add both methods to
FakeApiindispatcher.test.ts. Defaults:listReviewsreturnsthis.reviews(newReviewSummary[]field, default[]) andreviewerCanWritereturnsthis.canWrite(defaultok(true)). Add areviewChecks: string[]record of logins passed toreviewerCanWrite.Step 4: gate and seed review triggers (
dispatcher.ts,conversation-detect.ts)Ready.reasonand the localreasoningatetoTriggerReason | "review-requested" | "review-submitted". ExtendPayloadFieldswithreview?: { type?: unknown; content?: unknown } | nullandnumber?: unknown.Payloadalso intersects the newReviewSubmittedPayloadLike.gate, withconst reviewSubmitted = row.action === "reviewed":"review-submitted"; nodetect.commentAuthorcheck stays as is.pull_request.user.loginmust equal the agent (case-insensitive), else drop"review on a pull request not authored by the agent". Thenawait api.reviewerCanWrite(owner, repo, row.commentAuthor). Non-ok →this.fail(job, \could not check the reviewer's permission: ${status} ${message}`, "forgejo", null)and return null.false→ drop"reviewer lacks write access"`. The payload-permission block is skipped for review rows; extend its comment to say why.ownPrfor review rows is the author match from 3. TheOWN_PR_REVIEWbranch stays tied toreviewRequest. Workspace matching is unchanged.ResolvedSessiongainscreated: boolean(true fromresolveSession's creation path, false fromreuseMapped).runReady: for"review-submitted", resolve the review first:api.listReviews, then the entry with the highestidwhose author matches the reviewer (case-insensitive) and whosestatematches the event (_approved→APPROVED,_rejected→REQUEST_CHANGES,_comment→COMMENT). A failure or no match logs a warning and usesnull. The seed isbuildReviewSubmittedSeed({ repo, number, payload, review, catchUp: created, linkedIssue }), wherelinkedIssueis the issue number used in step 5 (null for PR-only or reused sessions).conversation-detect.ts: add the exportedReviewSubmittedPayloadLike(pull_requestas inReviewRequestPayloadLike, plussender,review?: { type?: string; content?: string | null }) andLines joined by
\n, with the body fenced viafenceFor:Review submitted on your pull request ${repo} #${number}: "<title>"Branch: <base.ref> ← <head.ref> (<head.sha first 7>) · <html_url>Reviewer: @<sender> · Verdict: <approved|changes requested|comment>, then either· Review id ${id} · ${commentsCount} line comment(s) · submitted ${submittedAt}or· Review id unknown: it is the newest review by @<sender> in forgejo_list_pr_comments_and_reviews.catchUp:This session is new and has no history for this pull request. Before acting, read it with forgejo_read_pr, the diff with forgejo_get_pr_diff and the conversation with forgejo_list_pr_comments_and_reviews+ (linkedIssue ?, and issue #${linkedIssue} with forgejo_read_issue: "") +. The checkout is detached at the PR head: create local branch <head.ref> at HEAD with git_branch before committing, so git_push updates the PR branch.Task: read the whole review and its line comments with forgejo_list_pr_comments_and_reviews. Address the feedback: change the code, commit, push. Reply in every line-comment thread of this review with forgejo_create_pr_thread_comment, naming what changed and the commit sha, or why you did not change it. Post one forgejo_create_pr_comment only if the review body itself has feedback, answering it. If the verdict is approved and there is no feedback, do nothing: no comment, no push.Review body:, fence, body or(empty), fence.Step 5: route reviews to an implementer session (
dispatcher.ts)reuseMapped: for"review-submitted"rows, a mapped session whosetrigger.kind !== "forgejo_implementer"returns"it is not an implementer session". A conversation/PR session lacks write tools, so a new implementer session replaces the mapping. Other reasons are unchanged.resolveSession: for"review-submitted"rows, the creation path builds an implementer session instead of the PR session:linkedIssue= first number fromextractLinkedIssues(payload.pull_request?.body)(apps/server/src/forgejo/tools/format.ts), else null.{ kind: "forgejo_implementer", scope: { owner, repo, number: linkedIssue ?? row.number, baseBranch: payload.pull_request?.base?.ref ?? null, pull: row.number } }.createCheckout({ workspacePath, owner, repo, kind: "pr", number: row.number }); same error wrapping andremoveCheckouton a failedcreateSessionas today.setForgejoSession(row.repo, row.number, id). WhenlinkedIssueis set andgetForgejoSession(row.repo, linkedIssue)is null or!this.isUsableTriggerSession(it), also map the issue. An issue already owned by a usable session keeps its mapping.Implement: ${row.repo}#${row.number}(startImplementer's style).{ handle, broughtLive: true, kind: "forgejo_implementer", created: true }and exposelinkedIssuetorunReady(return it onResolvedSessionaslinkedIssue: number | null; null on the reuse path).SESSION_POLICIES[kind].turnTimeoutMs(2 h for implementers).Tests
apps/server/src/forgejo/webhook.test.ts: each of the three review types withaction: "reviewed"→ accept (action: "reviewed",isPull: true,commentAuthor= sender). Self-authored → ignore;action: "submitted"→ ignore; missing number → malformed. Identical bodies → equal dedupeKey; differentreview.content→ different key.resolveEventTypemaps the three bare events.apps/server/src/routes-forgejo-webhook.test.ts: replace the two tests that pin the old ignore behavior ("without Event-Type, a pull_request_comment event is an inline review comment and ignored"and"pull_request_review_comment: 200 ignored") with acceptance tests: a signed review payload → 202 accepted, and bare Eventpull_request_comment→ accepted with eventpull_request_review_comment. Adjust thex-forgejo-eventdefault in theposthelper (~L70) for the approved/rejected types.apps/server/src/forgejo/dispatcher.test.ts, newdescribe("ForgejoDispatcher review submissions"):setForgejoSessionas in the existing startImplementer routing test ~L896) → the turn goes to it; the seed hasReview idand no catch-up paragraph; the row is done withreview-submitted.Closes: #4→ aforgejo_implementersession with scope{ number: 4, pull: 7, baseBranch: <base.ref> }and checkout kindpr; the seed has the catch-up paragraph naming#4; #7 and #4 are mapped to it.{ number: 7, pull: 7 }.forgejo_pull_requestsession → replaced by an implementer session; mapping overwritten.review on a pull request not authored by the agent;reviewChecksempty.permissions.push: truebutcanWrite = ok(false)→ droppedreviewer lacks write access.canWritenon-ok → failed, failure comment posted.listReviewsfailing → the turn runs and the seed saysReview id unknown.reviewerCanWritedecision paths (200 write/read, 403 → owner, 403 → team member, 403 → no team, 403 → teams list 404): unit-test them if aForgejoClientstub pattern exists (checkapps/server/src/forgejo/client.test.ts). Otherwise check them once with a throwaway script and do not commit it.Verification
cd apps/server && bun test src/forgejo src/routes-forgejo-webhook.test.ts src/bridge→ pass; then the fullbun testinapps/serverand the repo's typecheck script (rootpackage.json).reviewer lacks write access. Review a PR that the agent account opened outside any implementer session (noforgejo_sessionsrow for it) → a new implementer session with the catch-up seed in thepr-<n>clone; after the turn ends and the session is disposed, a second review resumes it and restores that clone.Blocked: this depends on implementer split 2/3, which hasn't landed.
ForgejoImplementerScope(apps/server/src/bridge/session-policy.ts) has no pull request number, and nothing maps an implementer session to the pull request it opens. So a review can't be routed to the implementer session yet.Resolution: paused until split 2/3 lands; this will be re-triggered then. No code changes were made.