feat(service,features,desktop): change an agent's pull request as the member who presses
W1 of the "Act on the PR" stack: the shared path for GitHub writes on a coding agent's pull request, tried out first on Ready for review and Convert to draft.
Who a write acts as
Every write acts as the workspace member who presses the button, using their own GitHub sign-in. It never falls back to the Luke GitHub App.
- First the agent is reached the same way the reads reach it (
agent-reach.ts, thenagentPullRequestReach). If the workspace's installation no longer reaches the repository, the write is refused before the member's token is read. - The token is the member's own GitHub App user token (
GitHubApp.userToken). That is the same token an agent uses to open its pull request as its developer. - If the member has no sign-in GitHub accepts, or GitHub answers 401, the answer is
sign-in-required. The tab then offers Sign in with GitHub, which is the account's GitHub connect. - If GitHub refuses the change itself (GraphQL errors, or a 403/422 with a message), the answer is
refused, carrying GitHub's own words.
The seam (for the stacks after this one)
packages/service/src/github/pull-request-writes.ts: one GraphQL write on the member's token (githubWrite), which tells the refusals apart. It also holdspullRequestNodeIdandsetPullRequestDraft(markPullRequestReadyForReview/convertPullRequestToDraft).packages/service/src/agents/pull-request-write.ts:memberPullRequestWrite(userId, agent, number, write)covers reach, the member's token and the write, and returns how it went. Review/comment, reactions, viewed marks and the pickers can reuse it.POST /api/agents/{id}/pull-request/action: its request and answer are declared in the newpull-request-write-wire.ts, with goldens. It is wired through the client, gateway, host and main asCODING_AGENTS_PULL_REQUEST_ACTION. After a write, the agent's kept published answer is dropped.
UI
In the fixed header's bar (#3250), the state pill becomes a dropdown, as stage-cli's is. It offers Ready for review for a draft and Convert to draft for an open pull request. Once merged or closed it is a plain pill again. When GitHub answers, the tab's reads run again, along with what the agent published, so the pill shows the new state. A refusal shows on a line under the bar until it is dismissed. All of this sits in pull-request-writes.tsx, so it moves with the header.




Tests
- Service (
coding-agents-app.test.ts, "changing an agent's pull request"):- a teammate's write goes out with the teammate's own token, never the App's;
- a member with no sign-in gets sign-in-required, and nothing is sent;
- a token GitHub refuses gets sign-in-required;
- GitHub's refusal comes back in its own words, both as GraphQL errors and as a 403;
- an account outside the workspace gets not-found, and an unknown action gets invalid-request.
- Host composer, main act row (voice window refused), and renderer tests: the pill's menu, reading again after a write, the Sign in with GitHub offer, and dismissing a refusal.
- Goldens: four new JSON Schemas, the wire's request and answer and the view's params and answer, recorded with
LUKE_UPDATE_FIXTURES=1. No existing golden changed. PRIVACY.mdnow says that a button press in the tab goes to GitHub as you, never as the App.
The full pnpm check:affected ran out of memory in the sandbox (exit 137). I ran the parts separately instead, and all passed: per-package tsc, oxlint, biome, knip, repository-checks.sh, discarded-effect, the affected vitest files, and the desktop build within its bundle budget.
Made with Luke
Summary by cubic
Lets a workspace member change a coding agent's pull request from its Pull request tab as themselves: the state pill becomes a dropdown offering Ready for review on a draft and Convert to draft on an open pull request, staying a plain pill once merged or closed.
- Each change is sent to GitHub on the pressing member's own GitHub App user token, never the Luke GitHub App's, after the repository's reach is checked first, as the reads check it.
- A member with no GitHub sign-in GitHub takes, or whose token GitHub refuses with a 401, is answered
sign-in-required, and the tab then offers Sign in with GitHub. - A change GitHub itself refuses — including a pull request GitHub answers as none — comes back in GitHub's own words, shown on a line under the bar until dismissed; after any answer, the tab reads the pull request again and every kept published answer for the agent is dropped, so the next read asks GitHub afresh.