autoland foundations: labctl PR review surface, ADR + CONTEXT.md, labctl-native land-pr #180

Closed
opened 2026-07-20 02:56:46 +02:00 by dominik.polakovics · 5 comments

What to build

The pre-engine foundations for the autoland (reject → fix-forward) pipeline: the agent-side PR review surface, the domain documentation, and a labctl-native landing playbook.

1. Tracker seam + labctl PR review verbs (completes the list ADR-0024 explicitly deferred):

  • labctl pr reject <n> <body> — post a Forgejo native changes-requested review with the findings as body
  • labctl pr rerequest <n> — re-request review (clears changes-requested); this becomes the fix run's done-signal later
  • labctl pr approve <n> [body] — post an approving review, making "validated, awaiting merge" forge-observable
  • labctl pr comment <n> <body> — plain PR discussion comment
  • labctl pr view <n> — additionally show reviews (state + body) so agents can read rejection findings

All on the ADR-0014 run-token boundary: Tracker seam methods + httpapi + labctl, mirroring how pr merge (ADR-0024) is wired.

2. Domain docs:

  • New ADR (companion to ADR-0024, which deferred "the reject → re-queue loop and pr close/pr comment") capturing the autoland design: state-derived poller (poll-only, no webhooks, per ADR-0015), lander + fix run kinds with explicit forge-observable done-signals, spawn-count attempt bound, agent-executed escalation, per-repo settings default-off.
  • CONTEXT.md: new concepts autoland, fix-forward, escalation; explicitly distinguish fix-forward (bounded re-engagement carrying new information — the rejection review — onto the existing claim branch) from the forbidden auto-requeue (blind retry, no new information). The existing auto-requeue grep guard must keep passing.

3. Validation core + land-pr skill:

  • Extract the single definition of "landable" (checks, conventions, conflict policy, PASS/CONCERNS/FAIL verdict rubric) into one tracked doc that both the interactive skill and the future lander seed template reference — one definition, no drift.
  • Rewrite assets/skills/land-pr onto labctl (tea is fully superseded); purge remaining tea references from docs/agents/issue-tracker.md and the skill.

Acceptance criteria

  • A run token can pr reject, pr rerequest, pr approve, pr comment, and see review state/bodies in pr view, covered by tests at the tracker seam + httpapi
  • ADR published; CONTEXT.md names autoland/fix-forward/escalation and distinguishes fix-forward from auto-requeue; the auto-requeue grep guard still passes
  • Validation-core doc exists and is referenced by the land-pr skill
  • land-pr skill and issue-tracker.md contain no tea references; the interactive skill works end-to-end over labctl

Blocked by

None - can start immediately

## What to build The pre-engine foundations for the autoland (reject → fix-forward) pipeline: the agent-side PR review surface, the domain documentation, and a labctl-native landing playbook. **1. Tracker seam + labctl PR review verbs** (completes the list ADR-0024 explicitly deferred): - `labctl pr reject <n> <body>` — post a Forgejo native changes-requested review with the findings as body - `labctl pr rerequest <n>` — re-request review (clears changes-requested); this becomes the fix run's done-signal later - `labctl pr approve <n> [body]` — post an approving review, making "validated, awaiting merge" forge-observable - `labctl pr comment <n> <body>` — plain PR discussion comment - `labctl pr view <n>` — additionally show reviews (state + body) so agents can read rejection findings All on the ADR-0014 run-token boundary: Tracker seam methods + httpapi + labctl, mirroring how `pr merge` (ADR-0024) is wired. **2. Domain docs:** - New ADR (companion to ADR-0024, which deferred "the reject → re-queue loop and pr close/pr comment") capturing the autoland design: state-derived poller (poll-only, no webhooks, per ADR-0015), lander + fix run kinds with explicit forge-observable done-signals, spawn-count attempt bound, agent-executed escalation, per-repo settings default-off. - CONTEXT.md: new concepts **autoland**, **fix-forward**, **escalation**; explicitly distinguish fix-forward (bounded re-engagement carrying new information — the rejection review — onto the existing claim branch) from the forbidden auto-requeue (blind retry, no new information). The existing auto-requeue grep guard must keep passing. **3. Validation core + land-pr skill:** - Extract the single definition of "landable" (checks, conventions, conflict policy, PASS/CONCERNS/FAIL verdict rubric) into one tracked doc that both the interactive skill and the future lander seed template reference — one definition, no drift. - Rewrite `assets/skills/land-pr` onto labctl (`tea` is fully superseded); purge remaining `tea` references from `docs/agents/issue-tracker.md` and the skill. ## Acceptance criteria - [ ] A run token can `pr reject`, `pr rerequest`, `pr approve`, `pr comment`, and see review state/bodies in `pr view`, covered by tests at the tracker seam + httpapi - [ ] ADR published; CONTEXT.md names autoland/fix-forward/escalation and distinguishes fix-forward from auto-requeue; the auto-requeue grep guard still passes - [ ] Validation-core doc exists and is referenced by the land-pr skill - [ ] land-pr skill and issue-tracker.md contain no `tea` references; the interactive skill works end-to-end over labctl ## Blocked by None - can start immediately
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: enhancement
Summary: Autoland foundations — labctl PR review verbs on the run-token boundary, the autoland ADR + CONTEXT.md concepts, and a labctl-native land-pr skill backed by a single validation-core doc.

Current behavior:
Run tokens can pr create, pr view, pr list, and pr merge — the merge verb is wired Tracker seam → httpapi run-token route → labctl (ADR-0024). There are no review verbs: an agent cannot post a changes-requested or approving review, re-request review, or comment on a PR, and pr view does not surface review state or bodies. ADR-0024 explicitly deferred "the reject → re-queue loop and pr close/pr comment". The land-pr skill and the issue-tracker agent doc still document tea, which is fully superseded by labctl, and the definition of "landable" (checks, conventions, conflict policy, verdict rubric) lives only inside the interactive skill text.

Desired behavior:

  1. Five new agent-facing PR verbs, all on the ADR-0014 run-token boundary, mirroring the pr merge wiring (Tracker seam method → httpapi → labctl):
    • labctl pr reject <n> <body> — Forgejo native changes-requested review, findings as body
    • labctl pr rerequest <n> — re-request review, clearing changes-requested (the future fix run's done-signal)
    • labctl pr approve <n> [body] — approving review ("validated, awaiting merge" becomes forge-observable)
    • labctl pr comment <n> <body> — plain PR discussion comment
    • labctl pr view <n> — additionally shows reviews (state + body) so agents can read rejection findings
  2. A new ADR (companion to ADR-0024) capturing the autoland design: state-derived poller (poll-only, no webhooks, consistent with ADR-0015), lander + fix run kinds with forge-observable done-signals, spawn-count attempt bound, agent-executed escalation, per-repo settings default-off.
  3. CONTEXT.md gains autoland, fix-forward, and escalation, explicitly distinguishing fix-forward (bounded re-engagement carrying new information — the rejection review — onto the existing claim branch) from the forbidden auto-requeue (blind retry, no new information). The existing auto-requeue grep guard must keep passing.
  4. One tracked validation-core doc defining "landable" (checks, conventions, conflict policy, PASS/CONCERNS/FAIL rubric), referenced by the land-pr skill now and the lander seed template later — one definition, no drift. The land-pr skill is rewritten onto labctl and all tea references are purged from it and from the issue-tracker agent doc.

Key interfaces:

  • tracker.Tracker seam — new review methods beside MergePull/CreatePull/Pull/Pulls; the pull view type carries review state + bodies
  • Forgejo native review API (REQUEST_CHANGES / APPROVED / re-request) — reviews are the forge-observable states the whole autoland loop keys on
  • httpapi run-token routes + labctl verbs, following the exact pr merge pattern
  • CONTEXT.md concept entries with Avoid lists; the grep-guard list is untouched
  • Validation-core doc as the single source for the PASS/CONCERNS/FAIL verdict rubric

Acceptance criteria:

  • A run token can pr reject, pr rerequest, pr approve, pr comment, and see review state/bodies in pr view, covered by tests at the tracker seam + httpapi
  • ADR published; CONTEXT.md names autoland/fix-forward/escalation and distinguishes fix-forward from auto-requeue; the auto-requeue grep guard still passes
  • Validation-core doc exists and is referenced by the land-pr skill
  • land-pr skill and the issue-tracker agent doc contain no tea references; the interactive skill works end-to-end over labctl

Out of scope:

  • The poller rule, settings, and lander run kind (#181)
  • Fix runs, attempt bound, escalation (#182)
  • pr close (still deferred), webhooks of any kind, changes to git push auth
> *This was generated by AI during triage.* ## Agent Brief **Category:** enhancement **Summary:** Autoland foundations — labctl PR review verbs on the run-token boundary, the autoland ADR + CONTEXT.md concepts, and a labctl-native land-pr skill backed by a single validation-core doc. **Current behavior:** Run tokens can `pr create`, `pr view`, `pr list`, and `pr merge` — the merge verb is wired Tracker seam → httpapi run-token route → labctl (ADR-0024). There are no review verbs: an agent cannot post a changes-requested or approving review, re-request review, or comment on a PR, and `pr view` does not surface review state or bodies. ADR-0024 explicitly deferred "the reject → re-queue loop and `pr close`/`pr comment`". The land-pr skill and the issue-tracker agent doc still document `tea`, which is fully superseded by labctl, and the definition of "landable" (checks, conventions, conflict policy, verdict rubric) lives only inside the interactive skill text. **Desired behavior:** 1. Five new agent-facing PR verbs, all on the ADR-0014 run-token boundary, mirroring the `pr merge` wiring (Tracker seam method → httpapi → labctl): - `labctl pr reject <n> <body>` — Forgejo native changes-requested review, findings as body - `labctl pr rerequest <n>` — re-request review, clearing changes-requested (the future fix run's done-signal) - `labctl pr approve <n> [body]` — approving review ("validated, awaiting merge" becomes forge-observable) - `labctl pr comment <n> <body>` — plain PR discussion comment - `labctl pr view <n>` — additionally shows reviews (state + body) so agents can read rejection findings 2. A new ADR (companion to ADR-0024) capturing the autoland design: state-derived poller (poll-only, no webhooks, consistent with ADR-0015), lander + fix run kinds with forge-observable done-signals, spawn-count attempt bound, agent-executed escalation, per-repo settings default-off. 3. CONTEXT.md gains **autoland**, **fix-forward**, and **escalation**, explicitly distinguishing fix-forward (bounded re-engagement carrying new information — the rejection review — onto the existing claim branch) from the forbidden auto-requeue (blind retry, no new information). The existing auto-requeue grep guard must keep passing. 4. One tracked validation-core doc defining "landable" (checks, conventions, conflict policy, PASS/CONCERNS/FAIL rubric), referenced by the land-pr skill now and the lander seed template later — one definition, no drift. The land-pr skill is rewritten onto labctl and all `tea` references are purged from it and from the issue-tracker agent doc. **Key interfaces:** - `tracker.Tracker` seam — new review methods beside `MergePull`/`CreatePull`/`Pull`/`Pulls`; the pull view type carries review state + bodies - Forgejo native review API (REQUEST_CHANGES / APPROVED / re-request) — reviews are the forge-observable states the whole autoland loop keys on - httpapi run-token routes + labctl verbs, following the exact `pr merge` pattern - CONTEXT.md concept entries with Avoid lists; the grep-guard list is untouched - Validation-core doc as the single source for the PASS/CONCERNS/FAIL verdict rubric **Acceptance criteria:** - [ ] A run token can `pr reject`, `pr rerequest`, `pr approve`, `pr comment`, and see review state/bodies in `pr view`, covered by tests at the tracker seam + httpapi - [ ] ADR published; CONTEXT.md names autoland/fix-forward/escalation and distinguishes fix-forward from auto-requeue; the auto-requeue grep guard still passes - [ ] Validation-core doc exists and is referenced by the land-pr skill - [ ] land-pr skill and the issue-tracker agent doc contain no `tea` references; the interactive skill works end-to-end over labctl **Out of scope:** - The poller rule, settings, and lander run kind (#181) - Fix runs, attempt bound, escalation (#182) - `pr close` (still deferred), webhooks of any kind, changes to git push auth
Author
Owner

Review brief for PR #183 (branch afk/180) — two changes before merge

Verdict from the 2026-07-20 review: one blocking code defect, one verified design gap to record. Amend the existing PR branch; everything else in the PR stands as-is — in particular handlePRGet's fail-the-whole-view on a reviews error and submitReview's any-non-404 → ErrReviewRejected mapping are documented-deliberate, leave them.

Change 1 — BLOCKING: changesRequestedReviewers must fold only verdict-bearing states

Files: internal/tracker/forgejo/forgejo.go (changesRequestedReviewers, ~line 826) and internal/tracker/github/github.go (~line 855). The two copies are deliberately parallel — fix both identically.

Defect: the fold skips only Dismissed and "pending", then does last-wins on every other state. A commented (or, Forgejo-only, review_requested) row arriving after a changes_requested row overwrites that reviewer's latest verdict and silently drops them from the set. Neither forge works that way: Forgejo's preparePullReviewType recognizes only APPROVED/REQUEST_CHANGES as verdict events (verified in the deployed v15.0.5 source), and GitHub's review decision is unaffected by COMMENTED reviews.

Failure it causes: reviewer requests changes, later replies in the thread (forge records a COMMENT review) → labctl pr rerequest <n> computes an empty set → returns the convergent no-op 200 while the forge still shows changes requested. The fix run's done-signal (ADR-0048) reports success on a PR that is still blocked, and the loop stalls permanently.

Fix: update latest[r.Reviewer] only when r.State is a verdict — tracker.ReviewApproved or tracker.ReviewChangesRequested. Keep the Dismissed skip. Non-verdict states (commented, review_requested, pending, anything unknown) must neither set nor clear a reviewer's verdict; the explicit "pending" skip is then subsumed. Accepted consequence, note it in the doc comment: after a successful rerequest the reviewer's latest verdict is still changes-requested, so a repeated pr rerequest re-POSTs the same reviewers instead of no-opping — that is faithful (they are still changes-requested until they re-review) and the forge handles a duplicate request idempotently. Update the doc comments on both copies and on both RerequestReview methods to match the new rule.

Tests (both backends): add the case that slipped — same reviewer REQUEST_CHANGES then COMMENT/COMMENTED → reviewer still in the POSTed set. Forgejo additionally: REQUEST_CHANGES then REQUEST_REVIEW → still in the set. Existing pins stay green: approve-after-reject drops out, dismissed skipped, pending-after-reject retained.

Change 2 — amend ADR-0048: lander verdicts require a second forge identity (prerequisite for #181)

No behavior change — the seam and its error mapping are correct. Record the verified deployment constraint in docs/adr/0048-autoland-fix-forward.md (a new pin bullet plus a status-line note is enough):

  • The deployed forge (Forgejo 15.0.5) unconditionally 422s approve ("approve your own pull is not allowed") and reject from the PR poster — preparePullReviewType, no configuration escape hatch — and every lab PR is authored by the single server-side forge-token account. Under one identity the lander's pr approve/pr reject are inert on 100% of lab PRs.
  • Pin: lander verdict verbs run under a distinct reviewer identity — a second forge account with write access on each autoland-enabled repo, its token held server-side, the lander run's tracker client constructed on it. ADR-0014's run-token boundary is unchanged: the server holds both forge credentials, agent sessions see neither.
  • Stays on the authoring token: pr create, pr merge (least-privilege — the reviewer account stays review-only), and the fix run's pr rerequest (the poster may re-request any reviewer with a prior review; verified against IsValidReviewRequest). pr comment and human reviews are unaffected.
  • #181 must not enable the lander verdict path on a repo until its reviewer credential is configured.

Done means

go test ./... green (the eight live-tmux internal/tmuxx failures are pre-existing sandbox noise), gofmt/go vet clean, both new fold tests in place, ADR-0048 amended, PR #183 updated in place on afk/180.

## Review brief for PR #183 (branch `afk/180`) — two changes before merge Verdict from the 2026-07-20 review: one blocking code defect, one verified design gap to record. Amend the existing PR branch; everything else in the PR stands as-is — in particular `handlePRGet`'s fail-the-whole-view on a reviews error and `submitReview`'s any-non-404 → `ErrReviewRejected` mapping are documented-deliberate, leave them. ### Change 1 — BLOCKING: `changesRequestedReviewers` must fold only verdict-bearing states **Files:** `internal/tracker/forgejo/forgejo.go` (`changesRequestedReviewers`, ~line 826) and `internal/tracker/github/github.go` (~line 855). The two copies are deliberately parallel — fix both identically. **Defect:** the fold skips only `Dismissed` and `"pending"`, then does last-wins on *every* other state. A `commented` (or, Forgejo-only, `review_requested`) row arriving after a `changes_requested` row overwrites that reviewer's latest verdict and silently drops them from the set. Neither forge works that way: Forgejo's `preparePullReviewType` recognizes only APPROVED/REQUEST_CHANGES as verdict events (verified in the deployed v15.0.5 source), and GitHub's review decision is unaffected by COMMENTED reviews. **Failure it causes:** reviewer requests changes, later replies in the thread (forge records a COMMENT review) → `labctl pr rerequest <n>` computes an empty set → returns the convergent no-op `200` while the forge still shows changes requested. The fix run's done-signal (ADR-0048) reports success on a PR that is still blocked, and the loop stalls permanently. **Fix:** update `latest[r.Reviewer]` **only** when `r.State` is a verdict — `tracker.ReviewApproved` or `tracker.ReviewChangesRequested`. Keep the `Dismissed` skip. Non-verdict states (`commented`, `review_requested`, `pending`, anything unknown) must neither set nor clear a reviewer's verdict; the explicit `"pending"` skip is then subsumed. Accepted consequence, note it in the doc comment: after a successful rerequest the reviewer's latest verdict is still changes-requested, so a repeated `pr rerequest` re-POSTs the same reviewers instead of no-opping — that is faithful (they *are* still changes-requested until they re-review) and the forge handles a duplicate request idempotently. Update the doc comments on both copies and on both `RerequestReview` methods to match the new rule. **Tests (both backends):** add the case that slipped — same reviewer REQUEST_CHANGES then COMMENT/COMMENTED → reviewer **still in** the POSTed set. Forgejo additionally: REQUEST_CHANGES then REQUEST_REVIEW → still in the set. Existing pins stay green: approve-after-reject drops out, dismissed skipped, pending-after-reject retained. ### Change 2 — amend ADR-0048: lander verdicts require a second forge identity (prerequisite for #181) No behavior change — the seam and its error mapping are correct. Record the verified deployment constraint in `docs/adr/0048-autoland-fix-forward.md` (a new pin bullet plus a status-line note is enough): - The deployed forge (Forgejo 15.0.5) **unconditionally 422s** approve ("approve your own pull is not allowed") and reject from the PR poster — `preparePullReviewType`, no configuration escape hatch — and every lab PR is authored by the single server-side forge-token account. Under one identity the lander's `pr approve`/`pr reject` are inert on 100% of lab PRs. - Pin: lander verdict verbs run under a **distinct reviewer identity** — a second forge account with write access on each autoland-enabled repo, its token held server-side, the lander run's tracker client constructed on it. ADR-0014's run-token boundary is unchanged: the server holds both forge credentials, agent sessions see neither. - Stays on the authoring token: `pr create`, `pr merge` (least-privilege — the reviewer account stays review-only), and the fix run's `pr rerequest` (the poster may re-request any reviewer with a prior review; verified against `IsValidReviewRequest`). `pr comment` and human reviews are unaffected. - #181 must not enable the lander verdict path on a repo until its reviewer credential is configured. ### Done means `go test ./...` green (the eight live-tmux `internal/tmuxx` failures are pre-existing sandbox noise), `gofmt`/`go vet` clean, both new fold tests in place, ADR-0048 amended, PR #183 updated in place on `afk/180`.
Author
Owner

REVISED review brief for PR #183 — supersedes the earlier 2026-07-20 brief above

Design change decided after review (grill, 2026-07-20): the lander's verdict channel moves from native forge reviews to anchored keyword PR comments — eliminating the second forge identity entirely. Background fact (verified in the deployed Forgejo 15.0.5 source and against the live forge): the forge unconditionally 422s approve/reject from the PR poster, and every lab PR is authored by the single server-side forge-token account, so native verdict verbs can never succeed same-identity. Comments on one's own PR are allowed, and merge has no self-restriction — so the comment channel works under one token, on any binding.

ADR-0048 ships inside this PR, so rewrite the affected pins in place — this is pre-merge editing, not an amendment.

Decisions to encode

  1. Verdict channel = anchored comment. A verdict comment's FIRST line is exactly [autoland] verdict: <word> with <word>reject | pass | fix-done. Parse rule: first line only, exact prefix match — a keyword quoted mid-body never triggers. Findings / CONCERNS prose follows below the marker line. Attempt counts stay derived from the runs store; any "(attempt 1/2)" suffix text is informational and never parsed. (Recommend #182's escalation digest use the same grammar, [autoland] verdict: escalate — note it in the ADR, implement there.)
  2. Hybrid verdict inputs. The loop's rejected-state folds BOTH sources: lander reject comments ∪ native changes-requested reviews (humans, forge bindings — the forge Review button works cross-identity and stays the human UX). Reviews() / pr view's review listing is the read for the native half; PR comments for the marker half.
  3. Fix-run done-signal = [autoland] verdict: fix-done comment, uniform on every binding. When human changes-requested reviewers exist, the fix run ALSO calls pr rerequest (best-effort forge ping to the human). Ownership rule, pinned in the ADR: fix-done clears a lander rejection (a fresh lander run re-validates); a human rejection is cleared only by that human's new review — the loop never merges over an outstanding human changes-requested.
  4. PASS. auto_merge on + clean PASS → merge, no comment (the merge is the observable state). auto_merge off, or PASS-with-CONCERNS → [autoland] verdict: pass comment (CONCERNS below the marker) = "validated, awaiting operator confirm"; the operator merges. Interactive land-pr skill: replace the approve-on-PASS step (which 409s on every lab PR today) with the same pass comment before the human confirm gate; FAIL stays report-in-session, no marker — an operator who wants autoland to pick a rejection up posts their own changes-requested review in the forge UI. Document the marker grammar once (validation-core.md is the natural home) and reference it from the skill.
  5. Strip pr reject and pr approve from this PR: the labctl verbs, agentapi handlers/routes, the RejectPull/ApprovePull seam methods on all three backends, the forgejo/github submitReview helper, and all their tests. They are correct code, but the loop never calls them and they fail same-identity by forge design; re-add if a multi-user deployment appears. Keep: the Review DTO, Reviews() + pr view's review section (hybrid read, decision 2), RerequestReview/pr rerequest (decision 3), CommentPull/pr comment (the verdict channel), and ErrReviewRejected (rerequest refusals still map to it).
  6. Builtin: feasible but deferred. The comment protocol removes the reason autoland was forge-only, but builtin's CommentPull stays ErrUnsupported in this PR. Rewrite the ADR's forge-only pin to: builtin joins once it grows a PR-comment write + comment listing — its own follow-up issue, not #181's scope.

Carried over from the original brief — still blocking

The changesRequestedReviewers fold fix stands unchanged (it feeds pr rerequest, which decision 3 keeps in the loop for human rejections): in internal/tracker/forgejo/forgejo.go (~826) and internal/tracker/github/github.go (~855), fold latest[r.Reviewer] only for verdict-bearing states (tracker.ReviewApproved, tracker.ReviewChangesRequested); keep the Dismissed skip; non-verdict states (commented, review_requested, pending, unknown) neither set nor clear a verdict. Accepted consequence (note in doc comments): repeat rerequest re-POSTs the same reviewers instead of no-opping — faithful, and idempotent at the forge. Tests in both backends: same reviewer REQUEST_CHANGES then COMMENT/COMMENTED → still in the POSTed set; Forgejo additionally REQUEST_CHANGES then REQUEST_REVIEW → still in the set; existing pins stay green.

Unchanged and deliberate — do not "fix": handlePRGet failing the whole view on a reviews-read error; rerequest's non-404 errors mapping to ErrReviewRejected.

Done means

Verbs stripped cleanly (go build/go vet/gofmt clean, no orphaned routes or docs), fold fix + new tests in both backends, ADR-0048 pins rewritten per decisions 1–6, land-pr skill + validation-core updated (pass-comment step, marker grammar), full go test ./... green (the eight live-tmux internal/tmuxx failures are pre-existing sandbox noise), PR #183 updated in place on afk/180. The PR body still advertises reject/approve — it can't be edited via the deployed labctl, so flag the stale body in your closing issue comment for the operator to update in the forge UI.

## REVISED review brief for PR #183 — supersedes the earlier 2026-07-20 brief above Design change decided after review (grill, 2026-07-20): the lander's verdict channel moves from **native forge reviews** to **anchored keyword PR comments** — eliminating the second forge identity entirely. Background fact (verified in the deployed Forgejo 15.0.5 source and against the live forge): the forge unconditionally 422s approve/reject from the PR poster, and every lab PR is authored by the single server-side forge-token account, so native verdict verbs can never succeed same-identity. Comments on one's own PR are allowed, and merge has no self-restriction — so the comment channel works under one token, on any binding. ADR-0048 ships **inside this PR**, so rewrite the affected pins in place — this is pre-merge editing, not an amendment. ### Decisions to encode 1. **Verdict channel = anchored comment.** A verdict comment's FIRST line is exactly `[autoland] verdict: <word>` with `<word>` ∈ `reject` | `pass` | `fix-done`. Parse rule: first line only, exact prefix match — a keyword quoted mid-body never triggers. Findings / CONCERNS prose follows below the marker line. Attempt counts stay derived from the runs store; any "(attempt 1/2)" suffix text is informational and never parsed. (Recommend #182's escalation digest use the same grammar, `[autoland] verdict: escalate` — note it in the ADR, implement there.) 2. **Hybrid verdict inputs.** The loop's rejected-state folds BOTH sources: lander `reject` comments ∪ native changes-requested reviews (humans, forge bindings — the forge Review button works cross-identity and stays the human UX). `Reviews()` / `pr view`'s review listing is the read for the native half; PR comments for the marker half. 3. **Fix-run done-signal = `[autoland] verdict: fix-done` comment**, uniform on every binding. When human changes-requested reviewers exist, the fix run ALSO calls `pr rerequest` (best-effort forge ping to the human). Ownership rule, pinned in the ADR: `fix-done` clears a *lander* rejection (a fresh lander run re-validates); a *human* rejection is cleared only by that human's new review — the loop never merges over an outstanding human changes-requested. 4. **PASS.** `auto_merge` on + clean PASS → merge, no comment (the merge is the observable state). `auto_merge` off, or PASS-with-CONCERNS → `[autoland] verdict: pass` comment (CONCERNS below the marker) = "validated, awaiting operator confirm"; the operator merges. **Interactive land-pr skill:** replace the approve-on-PASS step (which 409s on every lab PR today) with the same `pass` comment before the human confirm gate; FAIL stays report-in-session, no marker — an operator who wants autoland to pick a rejection up posts their own changes-requested review in the forge UI. Document the marker grammar once (validation-core.md is the natural home) and reference it from the skill. 5. **Strip `pr reject` and `pr approve` from this PR**: the labctl verbs, agentapi handlers/routes, the `RejectPull`/`ApprovePull` seam methods on all three backends, the forgejo/github `submitReview` helper, and all their tests. They are correct code, but the loop never calls them and they fail same-identity by forge design; re-add if a multi-user deployment appears. **Keep:** the `Review` DTO, `Reviews()` + `pr view`'s review section (hybrid read, decision 2), `RerequestReview`/`pr rerequest` (decision 3), `CommentPull`/`pr comment` (the verdict channel), and `ErrReviewRejected` (rerequest refusals still map to it). 6. **Builtin: feasible but deferred.** The comment protocol removes the *reason* autoland was forge-only, but builtin's `CommentPull` stays `ErrUnsupported` in this PR. Rewrite the ADR's forge-only pin to: builtin joins once it grows a PR-comment write + comment listing — its own follow-up issue, not #181's scope. ### Carried over from the original brief — still blocking **The `changesRequestedReviewers` fold fix stands unchanged** (it feeds `pr rerequest`, which decision 3 keeps in the loop for human rejections): in `internal/tracker/forgejo/forgejo.go` (~826) and `internal/tracker/github/github.go` (~855), fold `latest[r.Reviewer]` only for verdict-bearing states (`tracker.ReviewApproved`, `tracker.ReviewChangesRequested`); keep the `Dismissed` skip; non-verdict states (`commented`, `review_requested`, `pending`, unknown) neither set nor clear a verdict. Accepted consequence (note in doc comments): repeat `rerequest` re-POSTs the same reviewers instead of no-opping — faithful, and idempotent at the forge. Tests in both backends: same reviewer REQUEST_CHANGES then COMMENT/COMMENTED → still in the POSTed set; Forgejo additionally REQUEST_CHANGES then REQUEST_REVIEW → still in the set; existing pins stay green. Unchanged and deliberate — do not "fix": `handlePRGet` failing the whole view on a reviews-read error; rerequest's non-404 errors mapping to `ErrReviewRejected`. ### Done means Verbs stripped cleanly (`go build`/`go vet`/`gofmt` clean, no orphaned routes or docs), fold fix + new tests in both backends, ADR-0048 pins rewritten per decisions 1–6, land-pr skill + validation-core updated (pass-comment step, marker grammar), full `go test ./...` green (the eight live-tmux `internal/tmuxx` failures are pre-existing sandbox noise), PR #183 updated in place on `afk/180`. The PR body still advertises reject/approve — it can't be edited via the deployed labctl, so flag the stale body in your closing issue comment for the operator to update in the forge UI.
Author
Owner

FINAL review brief for PR #183 — supersedes BOTH earlier briefs in this thread

This brief is self-contained; ignore the two above except as history. Design finalized 2026-07-20 (grill + revision): verdicts ride anchored keyword PR comments, and the five labctl verbs all stay — the marker is a server-side implementation detail. Agents keep the semantic surface (reject/approve/rerequest) and never learn the grammar.

Why (verified): deployed Forgejo 15.0.5 unconditionally 422s approve/reject reviews from the PR poster (no config escape), and every lab PR is authored by the single server-side forge-token account — native review verdicts can never succeed same-identity. Comments on one's own PR are allowed and merge has no self-restriction, so the comment channel works under one token on any binding. A second bot account was considered and rejected.

Target verb behavior (labctl surface unchanged in shape)

  • pr reject <n> <body> → posts a PR comment: first line [autoland] verdict: reject, blank line, then the sanitized body. Body required (findings are the point).
  • pr approve <n> [body] → posts [autoland] verdict: pass + optional body (CONCERNS go here). This is "validated, awaiting confirm".
  • pr rerequest <n> → posts [autoland] verdict: fix-done (the fix run's done-signal), THEN best-effort native re-request of every reviewer whose latest verdict-bearing native review is changes-requested (the human forge ping). Comment first: it is the signal. A native-ping failure is non-fatal — warn, exit 0.
  • pr comment <n> <body> → plain comment, no marker (unchanged).
  • pr view → unchanged, including the reviews listing (the hybrid read of human native reviews).

Implementation decisions

  • Marker injection is server-side in the agentapi handlers (precedent: pr create's Closes-#N injection): handler composes marker + "\n\n" + sanitized body and calls the seam's CommentPull. A body that itself starts with the marker prefix lands below line 1 and is inert — no validation needed. Sanitizer and secretscan see the body exactly as any comment write.
  • Strip RejectPull/ApprovePull (+ the forgejo/github submitReview helper) from the Tracker seam and all three backends — verdict verbs become generic handler-level compositions over CommentPull. On builtin they now fail only because builtin CommentPull is ErrUnsupported; once builtin grows comments, the verdict verbs work there with zero further changes (record this in the ADR).
  • RerequestReview (native re-request) stays as a seam method on forgejo/github; the handler orchestrates comment-then-ping. ErrReviewRejected stays for its refusals.
  • Marker grammar (canonical home: ADR-0048): first line exactly [autoland] verdict: <word>, <word>reject | pass | fix-done (escalate reserved for #182). Consumers (#181's poller) match first line only, exact prefix — a keyword quoted mid-body never triggers. Attempt counts derive from the runs store, never parsed from comments. Skills and agent docs speak in verbs only and never spell the marker.

Loop semantics — rewrite ADR-0048 in place (it ships inside this PR; edit, don't amend)

  • Hybrid rejected-state = lander reject markers ∪ human native changes-requested reviews (the forge Review button works cross-identity and remains the human UX; Reviews() is the read for that half).
  • fix-done clears a lander rejection (a fresh lander run re-validates). A human rejection is cleared only by that human's new review — the loop never merges over an outstanding human changes-requested.
  • PASS: clean PASS + auto_merge on → merge directly, no comment (the merge is the observable state). Otherwise → pr approve (pass marker) and the operator confirms/merges.
  • Builtin autoland: feasible-but-deferred — gate is builtin CommentPull + PR-comment listing, its own follow-up issue, not #181's scope. The forge-only pin is rewritten to say so.
  • The "native forge reviews — not comments-as-protocol" pin is replaced by the above, with the verified single-identity rationale recorded under considered options (second bot account: rejected — ops burden per repo, does nothing for builtin, and the comment channel needs neither).

Blocking bug fix — unchanged from the first brief

changesRequestedReviewers in internal/tracker/forgejo/forgejo.go (~826) and internal/tracker/github/github.go (~855): fold latest[r.Reviewer] only for verdict-bearing states (tracker.ReviewApproved, tracker.ReviewChangesRequested); keep the Dismissed skip; commented/review_requested/pending/unknown neither set nor clear a verdict. Update both doc comments (accepted consequence: a repeated rerequest re-POSTs the same reviewers instead of no-opping — faithful, idempotent at the forge). Tests in both backends: same reviewer REQUEST_CHANGES then COMMENT/COMMENTED → still in the POSTed set; Forgejo additionally REQUEST_CHANGES then REQUEST_REVIEW → still in the set; existing pins stay green. This feeds the rerequest human-ping now and #181's poller fold later.

land-pr skill (assets/skills/land-pr/)

PASS → labctl pr approve <n> (CONCERNS as the body when present) before the human confirm gate — the verb now posts the pass comment under the hood, so the skill text stays verb-level. FAIL → report in session, no forge write; an operator who wants autoland to pick a rejection up posts their own changes-requested review in the forge UI. Neither SKILL.md nor validation-core.md documents the marker grammar — verbs only.

Do not "fix" (deliberate)

handlePRGet failing the whole view on a reviews-read error; RerequestReview mapping non-404 errors to ErrReviewRejected; the secretscan/incogni coverage patterns.

Done means

go build/go vet/gofmt clean; full go test ./... green (the eight internal/tmuxx live-tmux failures are pre-existing sandbox noise). Reject/approve reimplemented as comment compositions with agentapi-handler and labctl end-to-end tests asserting the marker first line; seam verdict methods stripped with no orphans; fold fix + new tests in both backends; ADR-0048 rewritten per above; skill updated. Push to afk/180 so PR #183 updates in place — no new PR. The PR body still describes native-review verdicts; flag the needed body edits in your closing comment on issue #180 for the operator to apply in the forge UI.

## FINAL review brief for PR #183 — supersedes BOTH earlier briefs in this thread This brief is self-contained; ignore the two above except as history. Design finalized 2026-07-20 (grill + revision): verdicts ride **anchored keyword PR comments**, and the **five labctl verbs all stay** — the marker is a server-side implementation detail. Agents keep the semantic surface (`reject`/`approve`/`rerequest`) and never learn the grammar. Why (verified): deployed Forgejo 15.0.5 unconditionally 422s approve/reject reviews from the PR poster (no config escape), and every lab PR is authored by the single server-side forge-token account — native review verdicts can never succeed same-identity. Comments on one's own PR are allowed and merge has no self-restriction, so the comment channel works under one token on any binding. A second bot account was considered and rejected. ### Target verb behavior (labctl surface unchanged in shape) - `pr reject <n> <body>` → posts a PR comment: first line `[autoland] verdict: reject`, blank line, then the sanitized body. Body required (findings are the point). - `pr approve <n> [body]` → posts `[autoland] verdict: pass` + optional body (CONCERNS go here). This is "validated, awaiting confirm". - `pr rerequest <n>` → posts `[autoland] verdict: fix-done` (the fix run's done-signal), THEN best-effort native re-request of every reviewer whose latest verdict-bearing native review is changes-requested (the human forge ping). Comment first: it is the signal. A native-ping failure is non-fatal — warn, exit 0. - `pr comment <n> <body>` → plain comment, no marker (unchanged). - `pr view` → unchanged, including the reviews listing (the hybrid read of human native reviews). ### Implementation decisions - **Marker injection is server-side** in the agentapi handlers (precedent: pr create's Closes-#N injection): handler composes `marker + "\n\n" + sanitized body` and calls the seam's `CommentPull`. A body that itself starts with the marker prefix lands below line 1 and is inert — no validation needed. Sanitizer and secretscan see the body exactly as any comment write. - **Strip `RejectPull`/`ApprovePull` (+ the forgejo/github `submitReview` helper) from the Tracker seam and all three backends** — verdict verbs become generic handler-level compositions over `CommentPull`. On builtin they now fail only because builtin `CommentPull` is `ErrUnsupported`; once builtin grows comments, the verdict verbs work there with zero further changes (record this in the ADR). - **`RerequestReview` (native re-request) stays** as a seam method on forgejo/github; the handler orchestrates comment-then-ping. `ErrReviewRejected` stays for its refusals. - **Marker grammar** (canonical home: ADR-0048): first line exactly `[autoland] verdict: <word>`, `<word>` ∈ `reject` | `pass` | `fix-done` (`escalate` reserved for #182). Consumers (#181's poller) match first line only, exact prefix — a keyword quoted mid-body never triggers. Attempt counts derive from the runs store, never parsed from comments. Skills and agent docs speak in verbs only and never spell the marker. ### Loop semantics — rewrite ADR-0048 in place (it ships inside this PR; edit, don't amend) - Hybrid rejected-state = lander `reject` markers ∪ human native changes-requested reviews (the forge Review button works cross-identity and remains the human UX; `Reviews()` is the read for that half). - `fix-done` clears a *lander* rejection (a fresh lander run re-validates). A *human* rejection is cleared only by that human's new review — the loop never merges over an outstanding human changes-requested. - PASS: clean PASS + `auto_merge` on → merge directly, no comment (the merge is the observable state). Otherwise → `pr approve` (pass marker) and the operator confirms/merges. - Builtin autoland: feasible-but-deferred — gate is builtin `CommentPull` + PR-comment listing, its own follow-up issue, not #181's scope. The forge-only pin is rewritten to say so. - The "native forge reviews — not comments-as-protocol" pin is replaced by the above, with the verified single-identity rationale recorded under considered options (second bot account: rejected — ops burden per repo, does nothing for builtin, and the comment channel needs neither). ### Blocking bug fix — unchanged from the first brief `changesRequestedReviewers` in `internal/tracker/forgejo/forgejo.go` (~826) and `internal/tracker/github/github.go` (~855): fold `latest[r.Reviewer]` **only** for verdict-bearing states (`tracker.ReviewApproved`, `tracker.ReviewChangesRequested`); keep the `Dismissed` skip; `commented`/`review_requested`/`pending`/unknown neither set nor clear a verdict. Update both doc comments (accepted consequence: a repeated `rerequest` re-POSTs the same reviewers instead of no-opping — faithful, idempotent at the forge). Tests in both backends: same reviewer REQUEST_CHANGES then COMMENT/COMMENTED → still in the POSTed set; Forgejo additionally REQUEST_CHANGES then REQUEST_REVIEW → still in the set; existing pins stay green. This feeds the rerequest human-ping now and #181's poller fold later. ### land-pr skill (assets/skills/land-pr/) PASS → `labctl pr approve <n>` (CONCERNS as the body when present) before the human confirm gate — the verb now posts the pass comment under the hood, so the skill text stays verb-level. FAIL → report in session, no forge write; an operator who wants autoland to pick a rejection up posts their own changes-requested review in the forge UI. Neither SKILL.md nor validation-core.md documents the marker grammar — verbs only. ### Do not "fix" (deliberate) `handlePRGet` failing the whole view on a reviews-read error; `RerequestReview` mapping non-404 errors to `ErrReviewRejected`; the secretscan/incogni coverage patterns. ### Done means `go build`/`go vet`/`gofmt` clean; full `go test ./...` green (the eight `internal/tmuxx` live-tmux failures are pre-existing sandbox noise). Reject/approve reimplemented as comment compositions with agentapi-handler and labctl end-to-end tests asserting the marker first line; seam verdict methods stripped with no orphans; fold fix + new tests in both backends; ADR-0048 rewritten per above; skill updated. Push to `afk/180` so PR #183 updates in place — no new PR. The PR body still describes native-review verdicts; flag the needed body edits in your closing comment on issue #180 for the operator to apply in the forge UI.
Author
Owner

This was generated by AI during the PR #183 rework.

Reworked PR #183 per the FINAL review brief (commit 71c9587 on afk/180, PR updated in place — no new PR).

Done:

  • Verdict verbs reimplemented as server-side marker-comment compositions in the agentapi handlers over CommentPull; all five labctl verbs keep their shape. pr rerequest posts the fix-done signal first, then best-effort native re-requests changes-requested human reviewers (ping failure = stderr warning, exit 0).
  • RejectPull/ApprovePull + the forgejo/github submitReview helper stripped from the seam, all three backends, and both decorators (metrics, secretscan) — no orphaned routes, docs, or tests. Review DTO, Reviews(), pr view's review listing, RerequestReview, CommentPull, and ErrReviewRejected kept.
  • Blocking changesRequestedReviewers fold fixed identically in both backends: only verdict-bearing states (approved/changes-requested) update a reviewer's latest verdict; Dismissed skip kept. New tests in both backends pin REQUEST_CHANGES→COMMENT/COMMENTED (and Forgejo REQUEST_CHANGES→REQUEST_REVIEW) → reviewer still in the POSTed set; existing pins stay green.
  • ADR-0048 rewritten in place per decisions 1–6; land-pr skill + validation-core updated (PASS → pr approve with CONCERNS as body before the confirm gate; FAIL → report in session, no forge write; no marker grammar outside the ADR).
  • go build/go vet/gofmt clean; full go test ./... green except the eight pre-existing internal/tmuxx live-tmux sandbox failures. Handler and labctl end-to-end tests assert the marker first line on reject/approve/rerequest.
  • Deliberate behaviors left untouched: handlePRGet fails the whole view on a reviews-read error; RerequestReview maps non-404 errors to ErrReviewRejected.

Operator action — the PR #183 body is stale (labctl cannot edit PR bodies). Please update it in the forge UI:

  • "Tracker seam: Review DTO + Reviews/RejectPull/ApprovePull/..." → the seam no longer has RejectPull/ApprovePull; reject/approve/rerequest are agentapi-handler compositions over CommentPull posting [autoland] verdict: marker comments (grammar pinned in ADR-0048), with RerequestReview kept as the best-effort native human-reviewer ping.
  • "POST /prs/{n}/reject|approve ... ErrReviewRejected/ErrUnsupported → 409" → reject/approve answer {number} (no review state); their failure modes are CommentPull's (404 unknown, 409 builtin-unsupported); rerequest answers {number, warning?} with ping failures non-fatal.
  • The ADR-0048 bullet should say: comment-marker verdict channel + hybrid rejected-state (lander markers ∪ human native changes-requested reviews), builtin autoland feasible-but-deferred behind builtin CommentPull, second bot account considered and rejected.
  • The land-pr bullet should note: interactive FAIL no longer writes to the forge (report-in-session); PASS posts the pass verdict via pr approve before the human confirm gate.
> *This was generated by AI during the PR #183 rework.* Reworked PR #183 per the FINAL review brief (commit 71c9587 on `afk/180`, PR updated in place — no new PR). **Done:** - Verdict verbs reimplemented as server-side marker-comment compositions in the agentapi handlers over `CommentPull`; all five labctl verbs keep their shape. `pr rerequest` posts the fix-done signal first, then best-effort native re-requests changes-requested human reviewers (ping failure = stderr warning, exit 0). - `RejectPull`/`ApprovePull` + the forgejo/github `submitReview` helper stripped from the seam, all three backends, and both decorators (metrics, secretscan) — no orphaned routes, docs, or tests. `Review` DTO, `Reviews()`, `pr view`'s review listing, `RerequestReview`, `CommentPull`, and `ErrReviewRejected` kept. - Blocking `changesRequestedReviewers` fold fixed identically in both backends: only verdict-bearing states (approved/changes-requested) update a reviewer's latest verdict; Dismissed skip kept. New tests in both backends pin REQUEST_CHANGES→COMMENT/COMMENTED (and Forgejo REQUEST_CHANGES→REQUEST_REVIEW) → reviewer still in the POSTed set; existing pins stay green. - ADR-0048 rewritten in place per decisions 1–6; land-pr skill + validation-core updated (PASS → `pr approve` with CONCERNS as body before the confirm gate; FAIL → report in session, no forge write; no marker grammar outside the ADR). - `go build`/`go vet`/`gofmt` clean; full `go test ./...` green except the eight pre-existing `internal/tmuxx` live-tmux sandbox failures. Handler and labctl end-to-end tests assert the marker first line on reject/approve/rerequest. - Deliberate behaviors left untouched: `handlePRGet` fails the whole view on a reviews-read error; `RerequestReview` maps non-404 errors to `ErrReviewRejected`. **Operator action — the PR #183 body is stale** (labctl cannot edit PR bodies). Please update it in the forge UI: - "Tracker seam: Review DTO + Reviews/RejectPull/ApprovePull/..." → the seam no longer has RejectPull/ApprovePull; reject/approve/rerequest are agentapi-handler compositions over CommentPull posting `[autoland] verdict:` marker comments (grammar pinned in ADR-0048), with RerequestReview kept as the best-effort native human-reviewer ping. - "POST /prs/{n}/reject|approve ... ErrReviewRejected/ErrUnsupported → 409" → reject/approve answer `{number}` (no review state); their failure modes are CommentPull's (404 unknown, 409 builtin-unsupported); rerequest answers `{number, warning?}` with ping failures non-fatal. - The ADR-0048 bullet should say: comment-marker verdict channel + hybrid rejected-state (lander markers ∪ human native changes-requested reviews), builtin autoland feasible-but-deferred behind builtin CommentPull, second bot account considered and rejected. - The land-pr bullet should note: interactive FAIL no longer writes to the forge (report-in-session); PASS posts the pass verdict via `pr approve` before the human confirm gate.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab#180
No description provided.