labctl: add a "pr merge" tool — no way to land a PR from the agent surface #37
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#37
Loading…
Add table
Add a link
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?
Problem
labctlcan open and inspect pull requests but cannot merge one. Theprsurface iscreate | view | list— write-once, then read-only. There is nopr merge, and the agent API (/agent/v1/*, mounted atinternal/httpapi/server.go:375) has no PR-merge route.Hit live this session: asked to "land" PR #36 (a clean fast-forward, CI + docs only), there was no
labctlpath to do it — the maintainer had to merge by hand in the forge UI.Why it matters
Landing a green PR is a routine end-of-task step, but today an agent has only two off-surface workarounds, both wrong:
POST /api/v1/repos/{owner}/{repo}/pulls/{n}/merge) — needs a forge token, which the agent session does not have (LAB_TOKENis a lab run token; no forge/API token in env, nocurl).main— bypasses the forge merge flow and branch protection; unacceptable for a protected trunk.Net: "land the PR" needs a human every time, even when the PR is trivially mergeable.
What exists today (the gap is uneven)
The server already has merge machinery — but only for the builtin / change-request binding:
internal/tracker/builtinimplementsMergeCR(...), exposed atPOST /api/v1/repos/{repo}/crs/{n}/merge(operator / CSRF surface), with both fast-forward and merge-commit paths — seeinternal/httpapi/crs_test.go(TestCRMergeFastForwardEndToEnd,TestCRMergeCommitPathUsesRepoIdentity).But the pieces needed for an agent-side merge are missing:
Trackerinterface (internal/tracker/tracker.go:160) hasCreatePull/CreateIssue/CreateCommentbut noMergePull— merge is not part of the binding contract.internal/tracker/forgejo/forgejo.go) only reads merged state (derivePullState, theMergedfield) — it has no merge action at all. For a forge-bound repo (this one) there is no code path to merge a PR.internal/labctl/client.go) has onlyPRCreate/PRView/PRList.So this is not "build merge from scratch" for the native binding — but the forge path, the one that actually mattered here, genuinely does not exist.
Possible shape (non-binding — for triage)
labctl pr merge <n>→ newPOST /agent/v1/prs/{n}/merge→ a newMergePullmethod on theTrackerinterface.MergeCR; forgejo calls the forge merge API using the server's forge credentials (so no token ever reaches the agent); github similarly, for parity.Open questions for triage
pr closetool (also missing — issues haveclose, PRs do not) or keep it merge-only?Agent Brief
Category: enhancement
Summary: Add
labctl pr merge <n>so an agent can land a mergeable PR/CR through the tracker seam, on both bindings, without a forge token ever reaching the session.Current behavior:
The agent PR surface is write-once-then-read-only:
labctl priscreate | view | list, backed byCreatePull/Pull/Pullson theTrackerseam and the run-token/agent/v1routes. There is no way to merge a PR from a session. Merge machinery does exist — but only for the builtin binding and only behind the operator (cookie + CSRF) surface: the change-request merge does a fast-forward whenorigin/<base>is an ancestor of the head, otherwise a--no-ffmerge commit built in a throwaway detached worktree, under a per-CR mutex, cancellation-immune once the git window opens, best-effort closing everyCloses #N, publishingcr.changed/issue.changed(seegitx.CRMerge+store.MergeCRand the operator CR-merge handler; ADR-0011). The abstractTrackerinterface has no merge method at all, and neither forge binding (Forgejo, GitHub) can merge — they only read merged state viaderivePullState. Net: landing a green PR always needs a human in the forge UI.Desired behavior:
labctl pr merge <n>merges PR/CRnand reports the result, working identically on a builtin-bound and a forge-bound repo, entirely over the run-token surface (no forge/API token in the session).Closes #Nclosure, change/issue events, convergent re-merge of an already-merged head) must hold whether the merge is invoked from the operator route or the new agent route. Expect to lift that orchestration into a shared service both surfaces call.Key interfaces:
Trackerseam — grows a merge method (e.g.MergePull(ctx, number) (PullRef, error)) alongsideCreatePull/Pull/Pulls. Callers speak PR numbers only, like the rest of the surface.Trackerimpl — itsMergePullroutes into the shared CR-merge service (the one the operator merge handler uses today), preserving the mutex / cancellation / close-issues / events behavior.Trackerimpls — gain a real merge action (today they only derive merged state). Server-credentialed forge merge call; map the forge's refusal to the seam's error verbatim./agent/v1for merge (mirroring the existing PR routes), returning the canonical error envelope and a pinned exit code on failure.labctlclient + CLI — apr merge <n>subcommand and a client method besidePRCreate/PRView/PRList.Acceptance criteria:
labctl pr merge <n>merges an open, mergeable PR/CR on a builtin-bound repo via the existing CR-merge path (ff-or-merge-commit), closing everyCloses #Nand emitting the same events the operator merge does.labctl pr merge <n>merges an open, mergeable PR/CR on a forge-bound repo via a server-side forge merge call, with no forge token present in the session environment.pr merge <n>on an already-merged PR/CR succeeds as a convergent no-op rather than erroring.Out of scope:
labctl pr close,pr comment, and the whole reject → re-queue-to-AFK loop (releasing the claim, feedback onto the issue, stopping condition) — tracked as a separate enhancement; do not build reject/close semantics here.--methodflag) — fixed ff-or-merge-commit only.