Commit

g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights

syntaqxcommitted Parentaa2cb24Browse files
59 files+470−790/59 viewed
+7−7
752752 "The check names reported on a repository's commits in the last 30 days, most recent first, with the events each was reported for: the names update_repo_settings takes in required_checks. A workflow's runs report a check named after the workflow; a check required on the default branch must be reported on a pull request's head (pull_request events) and, with the merge queue on, on its queued state (merge_group events)."
753753 }
754754 Op::MessageAgent => {
755− "Send the agent working on a pull request a message: a correction, a hint, a change of plan. It receives it at its next step, and it is recorded in the pull request's session. The pull request's author, and anyone with the Write role or higher. An agent uses it to ask the agent on another pull request a question (kind: question) or hand it work that belongs there (kind: handoff), giving its own pull request as from_number; the answer comes back to it at its next step."
755+ "Send the agent working on a pull request a message: a correction, a hint, a change of plan. It receives it at its next step, and it is recorded in the pull request's session. The pull request's author (for one g1t made, whoever asked for it), and anyone with the Write role or higher. An agent uses it to ask the agent on another pull request a question (kind: question) or hand it work that belongs there (kind: handoff), giving its own pull request as from_number; the answer comes back to it at its next step."
756756 }
757757 Op::AnswerMessage => {
758758 "Answer a question or a handoff another agent sent you, by the message's id. For a handoff, set decline to say it is not yours to take. The answer reaches the asking agent at its next step."
791791 "Open an issue on a repository. Say what done means in the body if it helps, for instance under a \"Definition of done\" heading; what must pass before a pull request for it merges is the default branch's required checks, the same for every pull request."
792792 }
793793 Op::UpdateIssue => {
794− "Change an issue's title, body, labels or the people it is assigned to. Only the fields given are changed; labels and assignees each replace the whole set. Its author may change their own issue; anyone else needs the Triage role or higher."
794+ "Change an issue's title, body, labels or the people it is assigned to. Only the fields given are changed; labels and assignees each replace the whole set. Its author may change their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher."
795795 }
796796 Op::CloseIssue => {
797− "Close an issue without a pull request. Merging a pull request made for an issue closes it for you. Its author may close their own issue; anyone else needs the Triage role or higher."
797+ "Close an issue without a pull request. Merging a pull request made for an issue closes it for you. Its author may close their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher."
798798 }
799− Op::ReopenIssue => "Reopen a closed issue. Its author may reopen their own issue; anyone else needs the Triage role or higher.",
799+ Op::ReopenIssue => "Reopen a closed issue. Its author may reopen their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher.",
800800 Op::PlanWork => {
801801 "Turn an outcome into a plan. An agent reads the repository and proposes the issues that would get there: what each changes, what done means for it (added to its body under \"Definition of done\"), the files it will touch, and which must merge before which. Returns the plan's id at once; the plan takes a minute or two to write, so read it with get_plan until its status is ready. Nothing is opened until apply_plan. Needs the Write role or higher."
802802 }
807807 "Open a plan's issues, each blocked by the ones it depends on. With assign, g1t agents start at once on every issue that depends on nothing, working in parallel, and on the others as what they depend on merges. keep limits it to some of the proposed issues, by their positions counting from 1. A plan is applied once. Needs the Write role or higher."
808808 }
809809 Op::AssignIssue => {
810− "Assign an issue to g1t. It opens a pull request for the issue in a sandbox of its own and sees it through: the repository's workflows run on it as its checks, a second agent reviews it, it revises when a check fails (reading the failing jobs' logs) or the review asks for changes, and it catches up when main moves. It is ready once the default branch's required checks pass and the review approves. Returns the pull request at once; follow its progress with get_pull_request. There is no model or agent count to choose. To put many agents to work, assign many issues. Needs the Write role or higher. In preview: only for accounts g1t agents are enabled for."
810+ "Assign an issue to g1t. It opens a pull request for the issue in a sandbox of its own and sees it through: the repository's workflows run on it as its checks, a second agent reviews it, it revises when a check fails (reading the failing jobs' logs) or the review asks for changes, and it catches up when main moves. It is ready once the default branch's required checks pass and the review approves. Returns the pull request at once, with g1t as its author and you as its requested_by; follow its progress with get_pull_request. There is no model or agent count to choose. To put many agents to work, assign many issues. Needs the Write role or higher. In preview: only for accounts g1t agents are enabled for."
811811 }
812812 Op::Delegate => {
813813 "Put an agent on something in one step: open an issue and assign it to g1t at once. Say what you want done in plain words, with what done means if you know it. What must pass before its pull request merges is the default branch's required checks. Needs the Write role or higher, and nothing is opened without it. The issue is opened whatever happens next: agent.status is started (pull is the draft pull request the agent opened; follow it with get_pull_request), queued (every agent slot of the workspace is busy; it starts by itself when one frees up) or not_started, with agent.code saying why (not_paid, trial_used, limit, paused, issue_cap, billing_unavailable or no_model), agent.message saying what to do, and agent.fix_url where. There is no model or agent count to choose."
817817 "Comment on an issue or a pull request. On a pull request, give path and line to comment on one line of the change."
818818 }
819819 Op::ReviewPullRequest => {
820− "Give a verdict on a pull request: approve it, or request changes and say what. Read get_pull_request_changes first. You cannot review a pull request you opened."
820+ "Give a verdict on a pull request: approve it, or request changes and say what. Read get_pull_request_changes first. You cannot review a pull request you opened, or one g1t made for you (you are its requested_by)."
821821 }
822822 Op::ListPullRequests => {
823823 "Pull requests on a repository, newest first. State open covers drafts and those ready for review; closed covers merged and closed."
835835 Op::MarkPullRequestReady => {
836836 "Mark a draft pull request ready for review. Push your commits first. The summary becomes its description and should say what changed and why."
837837 }
838− Op::ClosePullRequest => "Close a pull request without merging it. Its author may close their own; anyone else needs the Triage role or higher.",
838+ Op::ClosePullRequest => "Close a pull request without merging it. Its author may close their own, and whoever asked g1t for one may close that one; anyone else needs the Triage role or higher.",
839839 Op::GetPullRequestChanges => {
840840 "What a pull request changes: the files it touches and their line-by-line diff against the commit it started from. Use it to review a pull request or to compare several made for the same issue."
841841 }
+33−2
834834 "verified": false,
835835 "workspaces": []
836836 },
837+ "requested_by": null,
837838 "created_at": "2026-10-01T18:04:11.482Z",
838839 "updated_at": "2026-10-01T18:04:11.482Z",
839840 "closed_at": null,
874875 "verified": false,
875876 "workspaces": []
876877 },
878+ "requested_by": null,
877879 "created_at": "2026-10-01T18:04:11.482Z",
878880 "updated_at": "2026-10-01T18:04:11.482Z",
879881 "closed_at": null,
907909 "verified": false,
908910 "workspaces": []
909911 },
912+ "requested_by": null,
910913 "created_at": "2026-10-01T18:04:11.482Z",
911914 "updated_at": "2026-10-01T18:52:17.093Z",
912915 "closed_at": "2026-10-01T18:52:17.093Z",
950953 "verified": false,
951954 "workspaces": []
952955 },
956+ "requested_by": null,
953957 "created_at": "2026-10-01T18:20:02.117Z",
954958 "updated_at": "2026-10-01T18:20:02.117Z"
955959 },
993997 "verified": false,
994998 "workspaces": []
995999 },
1000+ "requested_by": null,
9961001 "created_at": "2026-10-01T18:20:02.117Z",
9971002 "updated_at": "2026-10-01T18:52:17.093Z"
9981003 }
10481053 "verified": false,
10491054 "workspaces": []
10501055 },
1056+ "requested_by": null,
10511057 "created_at": "2026-10-01T18:04:11.482Z",
10521058 "updated_at": "2026-10-01T18:09:47.305Z",
10531059 "closed_at": null,
10841090 "verified": false,
10851091 "workspaces": []
10861092 },
1093+ "requested_by": null,
10871094 "created_at": "2026-10-01T18:04:11.482Z",
10881095 "updated_at": "2026-10-01T19:30:00.214Z",
10891096 "closed_at": "2026-10-01T19:30:00.214Z",
11151122 "verified": false,
11161123 "workspaces": []
11171124 },
1125+ "requested_by": null,
11181126 "created_at": "2026-10-01T18:04:11.482Z",
11191127 "updated_at": "2026-10-01T19:41:52.830Z",
11201128 "closed_at": null,
11561164 "assignees": [],
11571165 "reviewers": [],
11581166 "author": {
1167+ "id": "usr_g1t_agent",
1168+ "username": "g1t",
1169+ "kind": "agent",
1170+ "verified": false,
1171+ "workspaces": []
1172+ },
1173+ "requested_by": {
11591174 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
11601175 "username": "syntaqx",
11611176 "kind": "user",
11661181 "updated_at": "2026-10-01T18:20:02.117Z",
11671182 "confidence": null
11681183 },
1169− "notes": "The response is the pull request g1t opened, still a draft. Follow it with [get a pull request](/reference/api/pull-requests/get-pull-request/): `lifecycle` says what the agent is doing."
1184+ "notes": "The response is the pull request g1t opened, still a draft. g1t is its `author` and you are its `requested_by`: you may manage it as if you had opened it, and cannot approve it yourself. Follow it with [get a pull request](/reference/api/pull-requests/get-pull-request/): `lifecycle` says what the agent is doing."
11701185 },
11711186 "delegate": {
11721187 "request": {
11911206 "verified": false,
11921207 "workspaces": []
11931208 },
1209+ "requested_by": null,
11941210 "created_at": "2026-10-05T14:02:11.204Z",
11951211 "updated_at": "2026-10-05T14:02:11.204Z",
11961212 "closed_at": null,
12271243 "assignees": [],
12281244 "reviewers": [],
12291245 "author": {
1246+ "id": "usr_g1t_agent",
1247+ "username": "g1t",
1248+ "kind": "agent",
1249+ "verified": false,
1250+ "workspaces": []
1251+ },
1252+ "requested_by": {
12301253 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
12311254 "username": "syntaqx",
12321255 "kind": "user",
14571480 "verified": false,
14581481 "workspaces": []
14591482 },
1483+ "requested_by": null,
14601484 "created_at": "2026-10-01T18:20:02.117Z",
14611485 "updated_at": "2026-10-01T18:35:44.902Z"
14621486 }
15011525 "verified": false,
15021526 "workspaces": []
15031527 },
1528+ "requested_by": null,
15041529 "created_at": "2026-10-01T18:20:02.117Z",
15051530 "updated_at": "2026-10-01T18:20:02.117Z"
15061531 },
15541579 "verified": false,
15551580 "workspaces": []
15561581 },
1582+ "requested_by": null,
15571583 "created_at": "2026-10-01T18:20:02.117Z",
15581584 "updated_at": "2026-10-01T18:35:44.902Z",
15591585 "confidence": null
15771603 "verified": false,
15781604 "workspaces": []
15791605 },
1606+ "requested_by": null,
15801607 "created_at": "2026-10-01T18:04:11.482Z",
15811608 "updated_at": "2026-10-01T18:20:02.117Z",
15821609 "closed_at": null,
17351762 "verified": false,
17361763 "workspaces": []
17371764 },
1765+ "requested_by": null,
17381766 "created_at": "2026-10-01T18:20:02.117Z",
17391767 "updated_at": "2026-10-01T18:33:10.398Z"
17401768 }
18101838 "verified": false,
18111839 "workspaces": []
18121840 },
1841+ "requested_by": null,
18131842 "created_at": "2026-10-01T18:20:02.117Z",
18141843 "updated_at": "2026-10-01T18:52:17.093Z"
18151844 },
18541883 "verified": false,
18551884 "workspaces": []
18561885 },
1886+ "requested_by": null,
18571887 "created_at": "2026-10-01T18:20:02.117Z",
18581888 "updated_at": "2026-10-01T19:02:48.760Z"
18591889 }
36453675 "number": null,
36463676 "state": null,
36473677 "author": null,
3678+ "requested_by": null,
36483679 "labels": [],
36493680 "topics": [],
36503681 "slug": null,
36543685 ],
36553686 "notes": []
36563687 },
3657− "notes": "`q` takes words, `\"exact phrases\"`, `-words` to leave out, and qualifiers: `repo:owner/name`, `org:` (or `workspace:`), `language:`, `path:` (a glob when it has `*`), `is:issue`, `is:pr`, `is:open`, `is:closed`, `is:merged`, `is:draft`, `is:public`, `is:private`, `author:` and `label:`; most can be left out with a leading `-`, as in `-label:wontfix`. `type` is `repositories`, `code`, `issues`, `pulls` or `people`; without it, `path:` means code, `is:pr` pull requests, `is:open`, `author:` or `label:` issues, and anything else repositories. `counts` says how many results each type has, up to 1,000. Each result's `snippet` (or, for code, each of its `lines`) is a list of parts, `highlight` true where the query matched. Code is searched on default branches and needs a word of three characters or more, unless the query names a `repo:`. Public content is returned to anyone, without a token; private content only to people who can read it (members of its workspace, and people given a role on the repository), checked when the search runs, so a repository made private, or a role taken away, stops appearing at once. A g1t agent's token can search too."
3688+ "notes": "`q` takes words, `\"exact phrases\"`, `-words` to leave out, and qualifiers: `repo:owner/name`, `org:` (or `workspace:`), `language:`, `path:` (a glob when it has `*`), `is:issue`, `is:pr`, `is:open`, `is:closed`, `is:merged`, `is:draft`, `is:public`, `is:private`, `author:` and `label:`; most can be left out with a leading `-`, as in `-label:wontfix`. `type` is `repositories`, `code`, `issues`, `pulls` or `people`; without it, `path:` means code, `is:pr` pull requests, `is:open`, `author:` or `label:` issues, and anything else repositories. `counts` says how many results each type has, up to 1,000. Each result's `snippet` (or, for code, each of its `lines`) is a list of parts, `highlight` true where the query matched. Code is searched on default branches and needs a word of three characters or more, unless the query names a `repo:`. Public content is returned to anyone, without a token; private content only to people who can read it (members of its workspace, and people given a role on the repository), checked when the search runs, so a repository made private, or a role taken away, stops appearing at once. A g1t agent's token can search too. An issue or pull request g1t opened has `author` `g1t` and `requested_by` the person it was for; `author:` matches the author."
36583689 },
36593690 "search_context": {
36603691 "params": {
+9−1
184184 marks it ready, which on g1t is when it first has code. Each head runs
185185 each workflow once.
186186
187+`github.event.pull_request` reads as it does on GitHub. For a pull request
188+g1t made, `pull_request.user` is g1t (`login` `g1t`, `type` `Bot`), and
189+`pull_request.requested_by` names the person who asked for it; it is `null`
190+on anyone else's. `github.event.issue.requested_by` does the same for an
191+issue g1t's agent filed. `sender` is whoever caused the event.
192+
187193 ## Checks
188194
189195 A pull request's checks are its workflows. Each workflow that runs on
255261 token for the run, with `GITHUB_TOKEN` as its alias. A pull request's runs
256262 get secrets and the token only when its author has the Write
257263 [role](/guides/access-and-roles/) or higher on the repository, a member or
258−an outside collaborator, or is g1t's agent. Anyone else's, such as one
264+an outside collaborator, or is g1t working on its own. For a pull request
265+g1t made, its author is g1t and the person who asked for it is the one
266+whose role counts. Anyone else's, such as one
259267 from a fork or by someone with Read or Triage, runs without secrets and
260268 with an empty token. See
261269 [who gets secrets](/guides/secrets-and-variables/#who-gets-secrets).
+2−1
314314 rather than the build. Secrets reach a preview only when the pull
315315 request's author has Write or higher on the repository, whether a member
316316 or an [outside collaborator](/guides/access-and-roles/#outside-collaborators),
317−or it is g1t's own agent. A preview of anyone else's pull request, such as
317+or it is g1t working on its own. For a pull request g1t made, whoever
318+asked for it is the one whose role counts. A preview of anyone else's pull request, such as
318319 one from a fork or by someone with Read or Triage, is built and runs with
319320 variables only, no secrets.
320321
+2−1
116116 https://g1t.sh/pulls/<pull request id>.git
117117 ```
118118
119−Only whoever opened the pull request can push to it. Pushes to a fork
119+Only whoever opened the pull request can push to it, or, for one g1t
120+made, whoever asked for it. Pushes to a fork
120121 update the pull request's head commit on its page.
121122
122123 ## Limits
+10−4
88 whether it merges cleanly, and the button that merges it. Everything in it
99 updates by itself while something is still running.
1010
11+A pull request g1t made shows **g1t** as its author and **requested by**
12+the person who asked for it. That person can manage it as its author could,
13+and cannot approve it; see
14+[who a pull request is for](/guides/working-with-g1t/#who-a-pull-request-is-for).
15+
1116 ## Checks
1217
1318 A pull request's checks are the statuses reported on its head commit. Most
149154 - **Resolve with g1t.** g1t merges the target branch in,
150155 resolves the conflicts keeping what both sides meant, and pushes the
151156 result. It is told which files conflict. Available to whoever can push to
152− the pull request: for a pull request's fork, whoever opened it; for a
153− branch, anyone with the Write role or higher.
157+ the pull request: for a pull request's fork, whoever opened it (whoever
158+ asked g1t for one it made); for a branch, anyone with the Write role or
159+ higher.
154160 - **Resolve in the browser.** Coming soon.
155161 - **On the command line.** The box lists the commands, each with a copy
156162 button. For a pull request from a branch:
184190
185191 When the target branch has moved, the merge box says **main has moved since
186192 this was made**. Whoever can push to the pull request (whoever opened it,
187−for one in its own fork; anyone with the Write [role](/guides/access-and-roles/) or higher,
188−for a branch) can
193+or asked g1t for it, for one in its own fork; anyone with the Write
194+[role](/guides/access-and-roles/) or higher, for a branch) can
189195 press **Catch up with main now**:
190196
191197 1. **When the two changed different files**, g1t merges `main` in itself,
+1−1
7070 | `is:open`, `is:closed` | Issues and pull requests by state | Issues, pull requests |
7171 | `is:merged`, `is:draft` | Pull requests that were merged, or are still drafts | Pull requests |
7272 | `is:public`, `is:private` | Results from public or private repositories | Repositories, code, issues, pull requests |
73−| `author:ana` | Opened by someone | Issues, pull requests |
73+| `author:ana` | Opened by someone. What g1t opened is `author:g1t`, and shows who it was for | Issues, pull requests |
7474 | `label:bug` | With a label. Quote a label with spaces: `label:"good first issue"` | Issues, pull requests |
7575 | `type:code` | Which tab to open: `repositories`, `code`, `issues`, `pulls` or `people` | All |
7676
+3−2
7878 - pushes, schedules, manual runs and the merge queue;
7979 - pull requests whose author has the Write [role](/guides/access-and-roles/)
8080 or higher on the repository, a member or an
81− [outside collaborator](/guides/access-and-roles/#outside-collaborators);
82−- pull requests from g1t's agents.
81+ [outside collaborator](/guides/access-and-roles/#outside-collaborators).
82+ For one g1t made, that is whoever asked for it;
83+- pull requests g1t opened on its own, such as a security update.
8384
8485 Anyone else's pull request, such as one from a fork or by someone with
8586 Read or Triage (who may open one on a private repository too), runs its
+2−2
7575 | `repo.collaborator_added`, `repo.collaborator_role_changed`, `repo.collaborator_removed` | Someone was given a role on it, had their role changed, or lost it. `data.username`, `data.role`, `data.previous_role`. See [access and roles](/guides/access-and-roles/). |
7676 | `repo.archived`, `repo.unarchived` | It was made read-only, or writable again. |
7777 | `repo.deleted`, `repo.restored`, `repo.purged` | It was deleted, restored within its 30 days, or removed for good. |
78−| `issue.opened`, `issue.updated`, `issue.assigned`, `issue.closed`, `issue.reopened` | An issue changed. `data.number`; on close, `data.reason` and `data.resolved_by`. |
78+| `issue.opened`, `issue.updated`, `issue.assigned`, `issue.closed`, `issue.reopened` | An issue changed. `data.number` and `data.author` (`id` and `username`); on close, `data.reason` and `data.resolved_by`. For an issue g1t's agent filed while at work, `data.author` is g1t and `data.requested_by` is the person it was working for. |
7979 | `comment.created` | A comment or review on an issue or pull request. |
80−| `pull.opened`, `pull.ready`, `pull.updated`, `pull.merge_requested`, `pull.merged`, `pull.closed` | A pull request changed. `data.number`, `data.issue`; on merge, `data.commit`. On a change by g1t, once g1t has worked it out, `data.confidence`: `level` (`high`, `medium` or `low`), `reasons`, `self_reported`, `uncertain_about`, `run_id` and `assessed_at`. See [how sure the agent is](/guides/working-with-g1t/#how-sure-the-agent-is). |
80+| `pull.opened`, `pull.ready`, `pull.updated`, `pull.merge_requested`, `pull.merged`, `pull.closed` | A pull request changed. `data.number`, `data.issue` and `data.author` (`id` and `username`); on merge, `data.commit`. For a change g1t made, `data.author` is g1t and `data.requested_by` is the person who asked for it; `actor` is still whoever caused the event. On a change by g1t, once g1t has worked it out, `data.confidence`: `level` (`high`, `medium` or `low`), `reasons`, `self_reported`, `uncertain_about`, `run_id` and `assessed_at`. See [how sure the agent is](/guides/working-with-g1t/#how-sure-the-agent-is). |
8181 | `checks.completed` | A pull request's checks finished: every status on its head has reported and none is still pending, or the merge queue took it out. `data.number`, `data.commit`, and `data.status`, `passed` or `failed`. |
8282 | `review.completed` | g1t reviewed a pull request. `data.verdict`. |
8383 | `workflow.completed` | A [workflow](/guides/actions/) run finished. `data.workflow`, `data.conclusion`, `data.run_id`, `data.sha`, `data.pull`. |
+32−1
1313 commits, comments, reviews, plans and security updates, assignments,
1414 timeline events, the audit log, notifications and webhooks. A pull request
1515 g1t opens shows g1t as its author, with **requested by** naming the person
16−who asked for it.
16+who asked for it. See [who a pull request is for](#who-a-pull-request-is-for).
1717
1818 g1t's runs are paid for by the workspace they work for, so they need a
1919 paid workspace or the free trial; see [who can run agents](#who-can-run-agents)
479479 A pull request g1t opens has `g1t` as its author and as its `agent` in the
480480 API, and its commits are authored `g1t <g1t@users.noreply.g1t.sh>`.
481481
482+## Who a pull request is for
483+
484+g1t is the author of every pull request it makes and of every issue it
485+files while at work. The person who asked for the work, by assigning the
486+issue or handing g1t the task, is kept beside it as **requested by**. Work
487+g1t starts itself, such as a [security update](/guides/security/), names
488+nobody.
489+
490+| Where | Author | Who asked |
491+| --- | --- | --- |
492+| The pull request's page, lists and link previews | **g1t** | **requested by** *name*, or **for** *name* |
493+| The API and MCP (`get_pull_request`, `list_pull_requests`, `get_issue`) | `author`: `{ "username": "g1t", "kind": "agent" }` | `requested_by`, or `null` |
494+| [Webhooks](/guides/webhooks/) | `data.author` | `data.requested_by`, left out when nobody asked |
495+| [Actions](/guides/actions/) (`github.event`) | `pull_request.user`, a `Bot` named `g1t` | `pull_request.requested_by`; `sender` is whoever caused the event |
496+| [Search](/guides/search/) | `author:g1t` finds it | shown as **for** *name* |
497+
498+The person who asked answers for the pull request as its author would:
499+
500+- they can update, close and mark it ready, catch it up and steer its
501+ agent without the Triage role;
502+- they are never asked to review it, and they cannot approve it or
503+ request changes on it; nor does their approval count toward the
504+ repository's required approvals;
505+- it is on their own lists: what they are working on, and their profile;
506+- the sandboxes that work on it act as them, so it reaches what they can;
507+- its workflows and preview get secrets only when they have the Write
508+ role or higher, as theirs would.
509+
510+Being its author gives g1t nothing more: a review by g1t's agent still
511+counts where the repository lets an agent's approval count.
512+
482513 ## How model traffic is routed
483514
484515 g1t's runs send model requests to g1t's model proxy at
+2−1
373373
374374 - **Overview:** pull requests merged, open pull requests and issues
375375 opened, and your most recent activity.
376−- **Pull requests** and **Issues:** everything you opened, newest first,
376+- **Pull requests** and **Issues:** everything you opened, and what g1t
377+ opened for you, newest first,
377378 with filters beside the list for state (open, closed, merged), type,
378379 repository and sort order. Add `?tab=pulls&state=merged` and the like to
379380 link to a filtered list.
+1−1
301301 | [`record_session`](/reference/api/sessions/record-session/) | Append entries to a pull request's session. Each has `kind` and `text`, and `tool` for tool entries. | `repo`, `number`, `entries` | `pull_requests:write` |
302302 | [`read_session`](/reference/api/sessions/read-session/) | The recorded session, oldest first. `after` skips to entries after a sequence number. | `repo`, `number` | `pull_requests:read` |
303303 | [`ready`](/reference/api/pull-requests/mark-pull-request-ready/) | Mark a draft ready for review. The summary becomes its description. | `repo`, `number`, `summary` | `pull_requests:write` |
304−| [`review`](/reference/api/pull-requests/review-pull-request/) | `approve`, or `request_changes` with a `body`. Not on your own pull request. | `repo`, `number`, `verdict` | `pull_requests:write` |
304+| [`review`](/reference/api/pull-requests/review-pull-request/) | `approve`, or `request_changes` with a `body`. Not on your own pull request, nor one g1t made for you. | `repo`, `number`, `verdict` | `pull_requests:write` |
305305 | [`close`](/reference/api/pull-requests/close-pull-request/) | Close it without merging. | `repo`, `number` | `pull_requests:write` |
306306 | [`merge`](/reference/api/pull-requests/merge-pull-request/) | Land it on `main` and resolve its issue, or add it to the [merge queue](/guides/merge-queue/), once every [required check](/guides/pull-requests/#required-status-checks) has passed on its head. `ignore_checks` bypasses them where the repository allows it. Write role. | `repo`, `number` | `pull_requests:write` |
307307 | [`merge_queue`](/reference/api/pull-requests/get-merge-queue/) | The pull requests waiting to land, in order, each with the state it is tested in and how that went; then those that recently landed or left. | `repo` | `pull_requests:read` |
+0−0

Binary or large file; its contents are not shown.

+1−1
257257 <Sparkles size={11} />
258258 written by {current.pull.agent}
259259 </span>
260− {current.pull.agent === "g1t" && <span>for {current.pull.author}</span>}
260+ {current.pull.requestedBy && <span>for {current.pull.requestedBy}</span>}
261261 </>
262262 ) : (
263263 <span>by {current.pull.author} with {current.pull.agent}</span>
+11−3
66
77 import { Avatar, TimeAgo } from "./ui";
88 import { Badge } from "./ui/badge";
9+import { PersonLink } from "./work";
910
1011 /** Text with the parts that matched the query marked. */
1112 export function Highlighted({ parts, className }: { parts: Segment[]; className?: string }) {
158159 {hit.snippet.length > 0 && <Highlighted parts={hit.snippet} className="mt-1 block text-sm text-muted" />}
159160 <div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-faint">
160161 {hit.author && (
161− <Link to={`/u/${hit.author}`} className="hover:text-fg">
162− {hit.author}
163− </Link>
162+ <span>
163+ <PersonLink name={hit.author} className="hover:text-fg" />
164+ {hit.requestedBy && (
165+ <>
166+ {" "}
167+ for{" "}
168+ <PersonLink name={hit.requestedBy} className="hover:text-fg" />
169+ </>
170+ )}
171+ </span>
164172 )}
165173 {hit.labels.map((label) => (
166174 <span key={label} className="rounded-full px-2 py-px text-muted ring-1 ring-line">
+11−6
11 import assert from "node:assert/strict";
22 import { test } from "node:test";
33
4+import { workOwner } from "../../../../packages/contracts/src/work.ts";
45 import { madeByG1t, openedBy } from "./opened-by.ts";
56
6−const author = { id: "usr_1", username: "syntaqx" };
7+const syntaqx = { id: "usr_1", username: "syntaqx" };
8+const g1t = { id: "usr_g1t_agent", username: "g1t", kind: "agent" as const };
79
8−test("a pull request g1t's agent made in a fork shows g1t, requested by its author", () => {
9− const pull = { agent: "g1t", branch: null, author };
10+test("a pull request g1t made shows g1t, requested by whoever asked", () => {
11+ const pull = { agent: "g1t", branch: null, author: g1t, requestedBy: syntaqx };
1012 assert.equal(madeByG1t(pull), true);
1113 assert.deepEqual(openedBy(pull), { name: "g1t", requestedBy: "syntaqx" });
14+ // It is theirs to manage, as their own would be.
15+ assert.equal(workOwner(pull).id, "usr_1");
1216 });
1317
1418 test("anything else shows its author", () => {
1519 // Someone's own agent.
16− assert.deepEqual(openedBy({ agent: "claude-code", branch: null, author }), { name: "syntaqx", requestedBy: null });
20+ assert.deepEqual(openedBy({ author: syntaqx, requestedBy: null }), { name: "syntaqx", requestedBy: null });
1721 // A branch pushed to the repository, even with g1t named on it.
1822 assert.equal(madeByG1t({ agent: "g1t", branch: "fix-login" }), false);
19− assert.deepEqual(openedBy({ agent: "g1t", branch: "fix-login", author }), { name: "syntaqx", requestedBy: null });
23+ assert.equal(workOwner({ author: syntaqx, requestedBy: null }).id, "usr_1");
2024 });
2125
2226 test("work g1t started itself names nobody as asking", () => {
23− const pull = { agent: "g1t", branch: null, author: { id: "g1t", username: "g1t" } };
27+ const pull = { author: { id: "g1t", username: "g1t", kind: "system" as const }, requestedBy: null };
2428 assert.deepEqual(openedBy(pull), { name: "g1t", requestedBy: null });
29+ assert.equal(workOwner(pull).username, "g1t");
2530 });
+6−8
1212 }
1313
1414 /**
15− * Who shows as having opened a pull request. g1t for one its agent made,
16− * with the person who asked for the work as `requestedBy`; anyone else's
17− * is its author's, with no `requestedBy`. Only the face: who may change
18− * the pull request is still decided by its stored author.
15+ * Who opened an issue or a pull request, and who asked g1t for it, as
16+ * stored: g1t for what it made or filed, with the person it was for as
17+ * `requestedBy`; anyone else's, theirs alone. Who may change it is the
18+ * same pair read the other way round: `workOwner` in the contracts.
1919 */
20−export function openedBy(pull: Pick<Pull, "agent" | "branch" | "author">): { name: string; requestedBy: string | null } {
21− if (!madeByG1t(pull)) return { name: pull.author.username, requestedBy: null };
22− // Work g1t started itself, such as a security update's code change, was asked for by nobody else.
23− return { name: G1T, requestedBy: pull.author.username === G1T ? null : pull.author.username };
20+export function openedBy(item: Pick<Pull, "author" | "requestedBy">): { name: string; requestedBy: string | null } {
21+ return { name: item.author.username, requestedBy: item.requestedBy?.username ?? null };
2422 }
+5−3
1010 type RepoPath,
1111 REPO_ROLE_LABELS,
1212 isActiveRun,
13+ workOwner,
1314 } from "@g1t/contracts";
1415
1516 import type { Route } from "./+types/home";
240241
241242 const lower = username.toLowerCase();
242243 const openPulls = (perRepo ?? []).flatMap(({ repo, pulls }) => (pulls ?? []).map((pull) => ({ pull, repo })));
244+ // Never your own, nor one g1t made for you.
243245 const reviewRequested = openPulls.filter(
244246 ({ pull }) =>
245247 pull.status === "open" &&
246− pull.author.username.toLowerCase() !== lower &&
248+ workOwner(pull).username.toLowerCase() !== lower &&
247249 pull.reviewers.some((name) => name.toLowerCase() === lower),
248250 );
249251
258260 repo: pathOf(repo),
259261 ref: `#${pull.number}`,
260262 by: agentWork ? who(pull.agent) : who(pull.author.username),
261− for: agentWork ? pull.author.username : null,
263+ for: agentWork ? workOwner(pull).username : null,
262264 facts: pullFacts({
263265 checkStatus: pull.checkStatus,
264266 files: pull.files,
398400 key,
399401 kind: "review",
400402 title: pull.title,
401− detail: `${pull.author.username} asked for your review.`,
403+ detail: `${workOwner(pull).username} asked for your review.`,
402404 to: `/${repo.namespace}/${repo.name}/pull/${pull.number}`,
403405 action: "Review",
404406 at: Date.parse(pull.updatedAt),
+11−4
33 import { useEffect } from "react";
44 import { Form, Link, redirect, useNavigation, useRevalidator } from "react-router";
55
6−import { type Pull, PROVIDERS } from "@g1t/contracts";
6+import { type Pull, PROVIDERS, workOwner } from "@g1t/contracts";
77
88 import type { Route } from "./+types/issue";
99 import { excerpt, page } from "../../lib/meta";
5252 return page(args, {
5353 title: `${title}${params.owner}/${params.repo} · g1t`,
5454 description: issue
55− ? `${state} issue #${issue.number} on ${params.owner}/${params.repo}, opened by ${issue.author.username}.${body ? ` ${body}` : ""}`
55+ ? `${state} issue #${issue.number} on ${params.owner}/${params.repo}, opened by ${issue.author.username}${issue.requestedBy ? ` for ${issue.requestedBy.username}` : ""}.${body ? ` ${body}` : ""}`
5656 : null,
5757 // The card shows the title and the state.
5858 version: issue ? [issue.title, issue.state, issue.reason] : undefined,
9494 computeNote,
9595 links,
9696 members: members?.ok ? members.value.map((member) => member.username) : [],
97− // The author can close and reopen their own issue; Triage and up, anyone's.
98− canManage: viewer != null && (viewer.id === issue.author.id || can.triage),
97+ // The author can close and reopen their own issue, and whoever g1t's
98+ // agent filed one for, that one; Triage and up, anyone's.
99+ canManage: viewer != null && (viewer.id === workOwner(issue).id || can.triage),
99100 can,
100101 };
101102 }
283284 <Avatar name={issue.author.username} size={18} />
284285 <span>
285286 <PersonLink name={issue.author.username} className="font-medium text-fg hover:underline" /> opened this{" "}
287+ {issue.requestedBy && (
288+ <>
289+ for{" "}
290+ <PersonLink name={issue.requestedBy.username} className="font-medium text-fg-soft hover:underline" />{" "}
291+ </>
292+ )}
286293 <TimeAgo at={issue.createdAt} />
287294 </span>
288295 </span>
+1−0
219219 <span className="mt-0.5 block text-xs text-faint">
220220 #{issue.number} opened <TimeAgo at={issue.createdAt} /> by{" "}
221221 {issue.author.username}
222+ {issue.requestedBy && <> for {issue.requestedBy.username}</>}
222223 {issue.resolvedBy != null && (
223224 <span className="text-merged"> · resolved by #{issue.resolvedBy}</span>
224225 )}
+7−5
2121 import { type ReactNode, Suspense } from "react";
2222 import { Await, Form, Link, useNavigation } from "react-router";
2323
24−import { type AgentRun, type G1tEvent, type Memory, type Pull, RUN_KIND_LABEL, isActiveRun } from "@g1t/contracts";
24+import { type AgentRun, type G1tEvent, type Memory, type Pull, RUN_KIND_LABEL, isActiveRun, workOwner } from "@g1t/contracts";
2525
2626 import type { Route } from "./+types/overview";
2727 import { host, StatusDot } from "../../components/deploy";
252252 } else if (lifecycle?.stage === "ready") {
253253 needs.push({ key: `pull:${pull.number}`, kind: "ready", title: pull.title, detail: "Ready to land when you merge it.", to, action: "Merge", at: Date.parse(pull.updatedAt), where });
254254 }
255− if (me && pull.status === "open" && pull.author.username.toLowerCase() !== me && pull.reviewers.some((name) => name.toLowerCase() === me)) {
256− needs.push({ key: `review:${pull.number}`, kind: "review", title: pull.title, detail: `${pull.author.username} asked for your review.`, to, action: "Review", at: Date.parse(pull.updatedAt), where });
255+ // Never your own, nor one g1t made for you; whoever it is for asked.
256+ const owner = workOwner(pull).username;
257+ if (me && pull.status === "open" && owner.toLowerCase() !== me && pull.reviewers.some((name) => name.toLowerCase() === me)) {
258+ needs.push({ key: `review:${pull.number}`, kind: "review", title: pull.title, detail: `${owner} asked for your review.`, to, action: "Review", at: Date.parse(pull.updatedAt), where });
257259 }
258260 if (member && pull.status === "open" && pull.checkStatus === "failed" && !runOn.has(pull.number) && !lifecycle) {
259261 needs.push({ key: `checks:${pull.number}`, kind: "checks", title: pull.title, detail: "It failed in the merge queue and no agent is fixing it.", to, action: "See checks", at: Date.parse(pull.updatedAt), where });
836838 <span>·</span>
837839 <span className="inline-flex items-center gap-1">
838840 <Avatar name={pull.agent} size={13} />
839− {madeByG1t(pull)
840− ? `made by g1t for ${pull.author.username}`
841+ {pull.requestedBy
842+ ? `made by ${pull.author.username} for ${pull.requestedBy.username}`
841843 : byAgent
842844 ? `made by ${pull.agent}`
843845 : `by ${pull.author.username}`}
+10−7
3535 type SessionEntry,
3636 type Viewer,
3737 pullComparison,
38+ workOwner,
3839 } from "@g1t/contracts";
3940
4041 import type { Route } from "./+types/pull";
183184 viewer,
184185 // Write and up can merge.
185186 canMerge: can.merge,
186− // Triage and up manage anyone's pull request; its author, their own.
187− canManage: can.triage || (viewer != null && viewer.id === pull.author.id),
187+ // Triage and up manage anyone's pull request; its author, their own,
188+ // and whoever asked g1t for one, that one.
189+ canManage: can.triage || (viewer != null && viewer.id === workOwner(pull).id),
188190 // Telling its agent things, and re-running checks, spend compute: Write and up.
189191 canRun: can.run,
190192 // A catch-up is pushed as the viewer: a fork takes pushes only from
191− // whoever opened it, a branch from anyone who can push.
192− canUpdate: pull.fork ? viewer?.id === pull.author.id : can.push,
193+ // whoever it is for (who asked g1t for it, or its author), a branch
194+ // from anyone who can push.
195+ canUpdate: pull.fork ? viewer?.id === workOwner(pull).id : can.push,
193196 agentsEnabled,
194197 computeNote,
195198 members: members?.ok ? members.value.map((person) => person.username) : [],
571574 const collisions = overlaps.filter((other) => other.issue == null || other.issue !== pull.issue);
572575 const review = {
573576 changesUrl: here + "?tab=changes",
574− // Nobody reviews their own pull request.
575− canJudge: active && viewer != null && viewer.id !== pull.author.id,
577+ // Nobody reviews their own pull request, nor one g1t made for them.
578+ canJudge: active && viewer != null && viewer.id !== workOwner(pull).id,
576579 };
577580 // Everyone whose review was asked for, then anyone who reviewed unasked.
578581 const reviewerNames = [
12451248 )}
12461249 <PeoplePicker
12471250 name="reviewer"
1248− members={members.filter((name) => name !== pull.author.username)}
1251+ members={members.filter((name) => name !== workOwner(pull).username)}
12491252 chosen={pull.reviewers.filter((name) => name !== "g1t")}
12501253 />
12511254 <Button variant="quiet" type="submit">
+2−2
33
44 import type { Route } from "./+types/pulls";
55 import { page } from "../../lib/meta";
6−import { madeByG1t, openedBy } from "../../lib/opened-by";
6+import { openedBy } from "../../lib/opened-by";
77 import { ButtonLink, EmptyState, TimeAgo } from "../../components/ui";
88 import { CheckBadge } from "../../components/checks";
99 import { ChangeSize, PullIcon, StateTabs } from "../../components/work";
7676 <span className="mt-0.5 block text-xs text-faint">
7777 #{pull.number} opened <TimeAgo at={pull.createdAt} /> by{" "}
7878 {openedBy(pull).name}
79− {madeByG1t(pull) && <> for {pull.author.username}</>}
79+ {pull.requestedBy && <> for {pull.requestedBy.username}</>}
8080 {pull.issue != null && <> · for #{pull.issue}</>}
8181 {pull.supersededBy != null && <> · superseded by #{pull.supersededBy}</>}
8282 </span>
+1−0
8181 agent: pull.agent,
8282 runtime: pull.runtime,
8383 author: pull.author.username,
84+ requestedBy: pull.requestedBy?.username ?? null,
8485 mergedBy: pull.mergedBy,
8586 mergedAt: pull.mergedAt,
8687 },
+12−2
229229 large tier: planning, catching up and reviews of small changes that
230230 touch no sensitive path run small; making changes, other reviews, and
231231 any retry after a failed attempt run large.
232+- **Who a g1t pull request is for:** g1t is the `author` (`username`
233+ `g1t`, `kind` `agent`) of every pull request it makes and every issue it
234+ files at work; `requested_by` is the person who asked (null when nobody
235+ did, as for a security update). That person answers for it as an author
236+ would: they may update, close and steer it without Triage, cannot
237+ approve it, are never asked to review it, see it in their own lists, and
238+ its sandboxes, workflows and previews are trusted as they are. Webhooks
239+ carry `data.author` and `data.requested_by`; Actions payloads
240+ `pull_request.user` (a `Bot`) and `pull_request.requested_by`.
232241 - **Put an agent on something in one step:** `POST {repo}/issues/delegate`
233242 with `title` and `body` (what to do, in plain words, and what done
234243 means if you know it). It opens the issue and assigns g1t at once; it needs the
398407 `secrets.G1T_TOKEN` (the workspace's token; `GITHUB_TOKEN` is its alias),
399408 which cannot change secrets. A pull request's runs and preview are trusted
400409 only when its author has `write` or higher on the repository (a member or
401−an outside collaborator) or is g1t's agent; anyone else's run with config
402−only. Guide:
410+an outside collaborator), or is g1t working on its own; for one g1t made,
411+the role of whoever asked for it (`requested_by`) counts. Anyone else's run
412+with config only. Guide:
403413 https://docs.g1t.sh/guides/secrets-and-variables/
404414
405415 Self-hosted runners: a job with `runs-on: self-hosted` (or
+9−0
150150 pub username: String,
151151 }
152152
153+impl From<&User> for Principal {
154+ fn from(user: &User) -> Self {
155+ Principal {
156+ id: user.id.clone(),
157+ username: user.username.clone(),
158+ }
159+ }
160+}
161+
153162 /// Set on a [`User`] resolved from an agent's token: the composite
154163 /// identity, "g1t on behalf of syntaqx", and what it may do.
155164 #[derive(Clone, Debug, Serialize, Deserialize)]
+12−0
7979 pub issue_id: String,
8080 pub repo_id: String,
8181 pub number: u32,
82+ /// Who opened it: g1t, for one its agent filed while at work.
83+ #[serde(skip_serializing_if = "Option::is_none")]
84+ pub author: Option<crate::credentials::Principal>,
85+ /// For an issue g1t's agent filed: the person it was working for.
86+ #[serde(skip_serializing_if = "Option::is_none")]
87+ pub requested_by: Option<crate::credentials::Principal>,
8288 #[serde(skip_serializing_if = "Option::is_none")]
8389 pub title: Option<String>,
8490 /// On close: `completed` or `not_planned`.
100106 pub pull_id: String,
101107 pub repo_id: String,
102108 pub number: u32,
109+ /// Who opened it: g1t, for a change g1t made.
110+ #[serde(skip_serializing_if = "Option::is_none")]
111+ pub author: Option<crate::credentials::Principal>,
112+ /// For a change g1t made: the person who asked for it.
113+ #[serde(skip_serializing_if = "Option::is_none")]
114+ pub requested_by: Option<crate::credentials::Principal>,
103115 /// The number of the issue it is for.
104116 #[serde(skip_serializing_if = "Option::is_none")]
105117 pub issue: Option<u32>,
+5−1
155155 /// `closed` for a pull request.
156156 #[serde(default)]
157157 pub state: Option<String>,
158− /// Who opened the issue or pull request.
158+ /// Who opened the issue or pull request: `g1t` for one g1t made.
159159 #[serde(default)]
160160 pub author: Option<String>,
161+ /// For an issue or pull request g1t opened: who asked for it.
162+ #[serde(default)]
163+ pub requested_by: Option<String>,
161164 #[serde(default)]
162165 pub labels: Vec<String>,
163166 /// A repository's topics.
191194 number: None,
192195 state: None,
193196 author: None,
197+ requested_by: None,
194198 labels: Vec::new(),
195199 topics: Vec::new(),
196200 slug: None,
+205−6
6262 pub reason: Option<IssueReason>,
6363 /// The number of the pull request whose merge closed this issue.
6464 pub resolved_by: Option<u32>,
65+ /// Who opened it: a person, an integration, or g1t (`kind` `agent`)
66+ /// for one its agent filed while at work.
6567 pub author: User,
68+ /// For an issue g1t's agent filed: the person it was working for. They
69+ /// may manage it as its author could. See [`owner`](Issue::owner).
70+ #[serde(default)]
71+ pub requested_by: Option<User>,
6672 /// RFC 3339.
6773 pub created_at: String,
6874 /// RFC 3339.
176182 /// g1t agent was asked.
177183 #[serde(default)]
178184 pub reviewers: Vec<String>,
185+ /// Who opened it: a person, or g1t (`kind` `agent`, username `g1t`)
186+ /// for a change g1t made.
179187 pub author: User,
188+ /// For a change g1t made: the person who asked for it, by assigning
189+ /// an issue or handing g1t the work. They answer for it as its author
190+ /// would. See [`owner`](Pull::owner).
191+ #[serde(default)]
192+ pub requested_by: Option<User>,
180193 /// RFC 3339.
181194 pub created_at: String,
182195 /// RFC 3339.
263276 pub paths: Vec<String>,
264277 }
265278
279+impl Issue {
280+ /// Who the issue is theirs to manage: the person g1t's agent filed it
281+ /// for, or its author. They may edit, close and reopen it without the
282+ /// Triage role.
283+ pub fn owner(&self) -> &User {
284+ self.requested_by.as_ref().unwrap_or(&self.author)
285+ }
286+}
287+
266288 impl Pull {
289+ /// Who the pull request is theirs to answer for: whoever asked g1t to
290+ /// make it, or its author. Every rule that once read "its author" reads
291+ /// this: they may update, close and steer it, are never asked to review
292+ /// it and cannot approve it, see it as theirs, and a sandbox at work on
293+ /// it acts as them.
294+ pub fn owner(&self) -> &User {
295+ self.requested_by.as_ref().unwrap_or(&self.author)
296+ }
297+
298+ /// Whether `id` is its [`owner`](Pull::owner)'s.
299+ pub fn is_owned_by(&self, id: &str) -> bool {
300+ self.owner().id == id
301+ }
302+
267303 /// What to ask the repos service to see what this pull request changes.
268304 ///
269305 /// A fork is compared as a whole. A branch is compared by name while
290326 }
291327 }
292328
329+/// g1t's agent, as the author of what it opens: stored by
330+/// [`AGENT_ID`](crate::identity::AGENT_ID), shown as `g1t`.
331+pub fn g1t_author() -> User {
332+ User {
333+ id: crate::identity::AGENT_ID.to_owned(),
334+ username: crate::identity::AGENT_NAME.to_owned(),
335+ kind: crate::PrincipalKind::Agent,
336+ ..User::default()
337+ }
338+}
339+
340+/// Who is recorded as opening an issue or a pull request that `actor`
341+/// opens, and who asked for it: `(author, requested_by)`.
342+///
343+/// - g1t's agent, acting for someone through its token (an issue it files
344+/// while at work): g1t, requested by that person.
345+/// - A person having g1t make the change (`by_g1t`: a hosted g1t agent in
346+/// a fork of its own): g1t, requested by them.
347+/// - Anyone else, and g1t's own work that nobody asked for: the actor, and
348+/// nobody asking.
349+pub fn authorship(actor: &User, by_g1t: bool) -> (User, Option<User>) {
350+ let person = |id: &str, username: &str| User {
351+ id: id.to_owned(),
352+ username: username.to_owned(),
353+ kind: crate::PrincipalKind::User,
354+ ..User::default()
355+ };
356+ if actor.kind == crate::PrincipalKind::Agent {
357+ let asked = actor
358+ .acting
359+ .as_ref()
360+ .map(|acting| &acting.on_behalf_of)
361+ .filter(|on_behalf_of| !crate::system::is_system_id(&on_behalf_of.id))
362+ .map(|on_behalf_of| person(&on_behalf_of.id, &on_behalf_of.username));
363+ return (g1t_author(), asked);
364+ }
365+ if by_g1t && actor.kind != crate::PrincipalKind::System {
366+ return (g1t_author(), Some(person(&actor.id, &actor.username)));
367+ }
368+ let author = User {
369+ id: actor.id.clone(),
370+ username: actor.username.clone(),
371+ kind: actor.kind,
372+ ..User::default()
373+ };
374+ (author, None)
375+}
376+
293377 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
294378 #[serde(rename_all = "lowercase")]
295379 pub enum CheckStatus {
727811 pub branch: String,
728812 /// The change's commit.
729813 pub head: String,
730− /// Who opened the pull request, and so can read its source.
814+ /// Who the pull request is for ([`Pull::owner`]: whoever asked g1t for
815+ /// it, or its author), and so can read its source.
731816 pub author: User,
732817 }
733818
812897 }
813898
814899 /// `message_agent`: sends the agent working on a pull request a message.
815−/// The pull request's author and members of the workspace may. Returns
900+/// The pull request's owner ([`Pull::owner`]: whoever asked g1t for it, or
901+/// its author) and members of the workspace may. Returns
816902 /// `Outcome<AgentMessage>`.
817903 #[derive(Debug, Serialize, Deserialize)]
818904 pub struct MessageAgentArgs {
9191005 pub pull_id: String,
9201006 pub repo: RepoPath,
9211007 pub number: u32,
922− /// Who the pull request belongs to. Sandboxes act as them.
1008+ /// Who the pull request belongs to ([`Pull::owner`]: whoever asked g1t
1009+ /// for it, or its author). Sandboxes act as them.
9231010 pub author: User,
9241011 /// The repository holding the change: its fork, or the repository
9251012 /// itself for one made on a branch.
13011388 /// Also `catch_up_pull`: brings the pull request up to date with the
13021389 /// default branch without a sandbox where that is safe, as the repos
13031390 /// service's `update_pull_branch` does, after checking that `actor` may
1304−/// update it: whoever opened it for a fork, any member for a branch.
1391+/// update it: its owner ([`Pull::owner`]) for a fork, any member for a
1392+/// branch.
13051393 /// Returns `Outcome<repos::PullBranchUpdate>`; on `needs_agent` nothing was
13061394 /// pushed and the runner's `update` is the way on.
13071395 #[derive(Debug, Serialize, Deserialize)]
15361624 /// The repository holding the commit: the fork, or the repository itself.
15371625 pub source: RepoPath,
15381626 pub commit: String,
1539− /// Who opened the pull request, and so can read its source.
1627+ /// Who the pull request is for ([`Pull::owner`]: whoever asked g1t for
1628+ /// it, or its author), and so can read its source.
15401629 pub author: User,
15411630 /// Username of whoever wrote the checks: the issue's author.
15421631 pub requested_by: String,
16191708 pub description: String,
16201709 /// The issue the pull request is for, which says what it should achieve.
16211710 pub issue: Option<Issue>,
1622− /// Who opened the pull request, and so can read its source.
1711+ /// Who the pull request is for ([`Pull::owner`]: whoever asked g1t for
1712+ /// it, or its author), and so can read its source.
16231713 pub author: User,
16241714 /// The files it changes, as of its latest push: how large the change
16251715 /// is, which decides the model that reviews it.
20702160 assert_eq!(with_definition_of_done(" Text ", &[]), "Text");
20712161 }
20722162 }
2163+
2164+#[cfg(test)]
2165+mod authorship_tests {
2166+ use super::*;
2167+ use crate::PrincipalKind;
2168+ use crate::credentials::{Acting, Principal};
2169+ use crate::identity::{AGENT_ID, AgentScope};
2170+
2171+ fn person() -> User {
2172+ User {
2173+ id: "usr_1".into(),
2174+ username: "syntaqx".into(),
2175+ verified: true,
2176+ ..User::default()
2177+ }
2178+ }
2179+
2180+ fn agent_for(id: &str, username: &str) -> User {
2181+ User {
2182+ id: AGENT_ID.into(),
2183+ username: "g1t".into(),
2184+ kind: PrincipalKind::Agent,
2185+ acting: Some(Box::new(Acting {
2186+ credential_id: "tok_1".into(),
2187+ agent: "g1t".into(),
2188+ on_behalf_of: Principal { id: id.into(), username: username.into() },
2189+ scope: AgentScope {
2190+ repo: RepoPath { namespace: "acme".into(), name: "web".into() },
2191+ operations: Vec::new(),
2192+ run: None,
2193+ },
2194+ })),
2195+ ..User::default()
2196+ }
2197+ }
2198+
2199+ fn pull(author: User, requested_by: Option<User>) -> Pull {
2200+ let mut pull: Pull = serde_json::from_value(serde_json::json!({
2201+ "id": "pr_1", "repoId": "rep_1", "number": 14, "issue": 12, "title": "Fix it", "body": null,
2202+ "agent": "g1t", "runtime": "hosted", "status": "open", "fork": null, "forkRepoId": null,
2203+ "branch": null, "headCommit": null, "mergeBase": null, "mergedBy": null, "mergedAt": null,
2204+ "supersededBy": null, "checkStatus": null,
2205+ "author": { "id": "x", "username": "x" },
2206+ "createdAt": "", "updatedAt": ""
2207+ }))
2208+ .unwrap();
2209+ pull.author = author;
2210+ pull.requested_by = requested_by;
2211+ pull
2212+ }
2213+
2214+ #[test]
2215+ fn a_change_a_person_has_g1t_make_is_g1t_s_requested_by_them() {
2216+ let (author, asked) = authorship(&person(), true);
2217+ assert_eq!((author.id.as_str(), author.username.as_str(), author.kind), (AGENT_ID, "g1t", PrincipalKind::Agent));
2218+ let asked = asked.expect("the person asked for it");
2219+ assert_eq!((asked.id.as_str(), asked.username.as_str(), asked.kind), ("usr_1", "syntaqx", PrincipalKind::User));
2220+ }
2221+
2222+ #[test]
2223+ fn what_g1t_s_agent_files_at_work_is_g1t_s_requested_by_whoever_it_works_for() {
2224+ let (author, asked) = authorship(&agent_for("usr_1", "syntaqx"), false);
2225+ assert_eq!(author.id, AGENT_ID);
2226+ assert_eq!(asked.map(|user| user.username), Some("syntaqx".into()));
2227+ }
2228+
2229+ #[test]
2230+ fn nobody_asked_for_g1t_s_own_work() {
2231+ // A run g1t started itself acts for g1t, not for a person.
2232+ let (author, asked) = authorship(&agent_for(crate::system::ID, "g1t"), false);
2233+ assert_eq!(author.id, AGENT_ID);
2234+ assert!(asked.is_none());
2235+ // A security update g1t opens is g1t's own, as before.
2236+ let (author, asked) = authorship(&User::system("acme"), true);
2237+ assert_eq!((author.id.as_str(), author.kind), (crate::system::ID, PrincipalKind::System));
2238+ assert!(asked.is_none());
2239+ }
2240+
2241+ #[test]
2242+ fn anyone_else_opens_their_own() {
2243+ let (author, asked) = authorship(&person(), false);
2244+ assert_eq!((author.id.as_str(), author.kind), ("usr_1", PrincipalKind::User));
2245+ assert!(asked.is_none());
2246+ // Nothing but who they are is kept.
2247+ assert!(author.workspaces.is_empty() && author.acting.is_none());
2248+ }
2249+
2250+ #[test]
2251+ fn the_requester_owns_g1t_s_pull_request_and_an_author_their_own() {
2252+ let made = pull(g1t_author(), Some(person()));
2253+ assert_eq!(made.owner().id, "usr_1");
2254+ assert!(made.is_owned_by("usr_1"));
2255+ assert!(!made.is_owned_by(AGENT_ID), "g1t's agent does not answer for its own change");
2256+ let own = pull(person(), None);
2257+ assert_eq!(own.owner().id, "usr_1");
2258+ assert!(own.is_owned_by("usr_1"));
2259+ }
2260+
2261+ #[test]
2262+ fn requested_by_is_null_when_nobody_asked_and_read_as_absent_from_older_senders() {
2263+ let own = pull(person(), None);
2264+ let sent = serde_json::to_value(&own).unwrap();
2265+ assert!(sent["requestedBy"].is_null());
2266+ let mut older = sent.clone();
2267+ older.as_object_mut().unwrap().remove("requestedBy");
2268+ let read: Pull = serde_json::from_value(older).unwrap();
2269+ assert!(read.requested_by.is_none());
2270+ }
2271+}
+27−3
8282 * points to now, and `defaultBranch` whether it is the default branch.
8383 */
8484 "git.push": { repoId: string; ref: string; after: string; defaultBranch: boolean };
85− "issue.opened": { issueId: string; repoId: string; number: number; title: string };
85+ /**
86+ * `author` is who opened it: g1t, for one its agent filed while at work,
87+ * with `requestedBy` the person it was working for. Every issue and pull
88+ * request event carries both.
89+ */
90+ "issue.opened": {
91+ issueId: string;
92+ repoId: string;
93+ number: number;
94+ title: string;
95+ author?: { id: string; username: string };
96+ requestedBy?: { id: string; username: string };
97+ };
8698 "issue.updated": { issueId: string; repoId: string; number: number };
8799 /** The people an issue is assigned to changed; `assignees` is the new set. */
88100 "issue.assigned": { issueId: string; repoId: string; number: number; assignees: string[] };
95107 resolvedBy?: number;
96108 };
97109 "issue.reopened": { issueId: string; repoId: string; number: number };
98− /** `issue` is the number of the issue the pull request is for. */
99− "pull.opened": { pullId: string; repoId: string; number: number; issue?: number; agent: string };
110+ /**
111+ * `issue` is the number of the issue the pull request is for. `author` is
112+ * who opened it: g1t, for a change g1t made, with `requestedBy` the person
113+ * who asked for it.
114+ */
115+ "pull.opened": {
116+ pullId: string;
117+ repoId: string;
118+ number: number;
119+ issue?: number;
120+ agent: string;
121+ author?: { id: string; username: string };
122+ requestedBy?: { id: string; username: string };
123+ };
100124 /** `confidence`, on a g1t agent's change once g1t has worked it out, is on every pull request event. */
101125 "pull.ready": { pullId: string; repoId: string; number: number; issue?: number; confidence?: Confidence };
102126 /** A push moved the head of a pull request that is ready for review. */
+3−0
4848 number: number | null;
4949 /** `open`/`closed` for an issue; `draft`/`open`/`merged`/`closed` for a pull request. */
5050 state: string | null;
51+ /** Who opened the issue or pull request: `g1t` for one g1t made. */
5152 author: string | null;
53+ /** For an issue or pull request g1t opened: who asked for it. */
54+ requestedBy?: string | null;
5255 labels: string[];
5356 topics: string[];
5457 /** A username or a workspace's slug. */
+27−4
3333 reason: IssueReason | null;
3434 /** The number of the pull request whose merge closed this issue. */
3535 resolvedBy: number | null;
36+ /** Who opened it: a person, an integration, or g1t (`kind` `agent`) for one its agent filed while at work. */
3637 author: User;
38+ /**
39+ * For an issue g1t's agent filed: the person it was working for. They may
40+ * manage it as its author could. See `workOwner`.
41+ */
42+ requestedBy: User | null;
3743 /** RFC 3339. */
3844 createdAt: string;
3945 /** RFC 3339. */
5965 agent: string | null;
6066 };
6167
68+/**
69+ * Whose an issue or a pull request is to answer for: whoever asked g1t for
70+ * it, or its author. They may change, close and steer it, are never asked to
71+ * review it and cannot approve it, and see it as theirs. Mirrors
72+ * `Pull::owner` in the Rust contracts.
73+ */
74+export function workOwner(item: Pick<Pull, "author" | "requestedBy">): User {
75+ return item.requestedBy ?? item.author;
76+}
77+
6278 /** `draft` is still being worked on; `open` is ready for review. */
6379 export type PullStatus = "draft" | "open" | "merged" | "closed";
6480
118134 * agent was asked.
119135 */
120136 reviewers: string[];
137+ /** Who opened it: a person, or g1t (`kind` `agent`, username `g1t`) for a change g1t made. */
121138 author: User;
139+ /**
140+ * For a change g1t made: the person who asked for it, by assigning an issue
141+ * or handing g1t the work. They answer for it as its author would. See
142+ * `workOwner`.
143+ */
144+ requestedBy: User | null;
122145 /** RFC 3339. */
123146 createdAt: string;
124147 /** RFC 3339. */
292315 /** The repository holding the commit: the fork, or the repository itself. */
293316 source: RepoPath;
294317 commit: string;
295− /** Who opened the pull request, and so can read its source. */
318+ /** Who the pull request is for (`workOwner`: whoever asked g1t for it, or its author), and so can read its source. */
296319 author: User;
297320 /** Username of whoever wrote the checks: the issue's author. */
298321 requestedBy: string;
447470 branch: string;
448471 /** The change's commit. */
449472 head: string;
450− /** Who opened the pull request, and so can read its source. */
473+ /** Who the pull request is for (`workOwner`: whoever asked g1t for it, or its author), and so can read its source. */
451474 author: User;
452475 };
453476
494517 pullId: string;
495518 repo: RepoPath;
496519 number: number;
497− /** Who the pull request belongs to. Sandboxes act as them. */
520+ /** Who the pull request belongs to (`workOwner`: whoever asked g1t for it, or its author). Sandboxes act as them. */
498521 author: User;
499522 /** The repository holding the change: its fork, or the repository itself. */
500523 source: RepoPath;
658681 description: string;
659682 /** The issue the pull request is for, which says what it should achieve. */
660683 issue: Issue | null;
661− /** Who opened the pull request, and so can read its source. */
684+ /** Who the pull request is for (`workOwner`: whoever asked g1t for it, or its author), and so can read its source. */
662685 author: User;
663686 /**
664687 * The files it changes, as of its latest push: how large the change is,
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

This change is too large to show in full.