Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar
- Cards: agents' cards carry a preview, facts and actions answered by their owner. A session's card offers Message, Stop, Approve more (owners, amount inline) and Follow up; an agent drafts issues as cards anyone in the conversation files as themselves with one press (only where they can read), or discards. Chat checks who can read; agents checks the rest and updates the card in place - Agents comment on issues and pull requests, and review pull requests, as themselves on behalf of a person (work: workspace_agent_comment, workspace_agent_review); their reviews are advisory and never count toward required approvals or code owners; review routines post on the pull request - Chat shows people by display name everywhere through one rule; mentions read as names; the API and MCP carry display_username beside username - The contribution calendar counts commits pushed to the default branch (repos: push_commits, commit_days)
| 167 | 167 | None => Decision::allow("anonymous"), | |
| 168 | 168 | }); | |
| 169 | 169 | record(op, services, viewer, input, &decision, Some(&outcome)).await; | |
| 170 | − | Ok(outcome) | |
| 170 | + | // Every person in the answer with the case they chose (people.rs). | |
| 171 | + | Ok(match outcome { | |
| 172 | + | Outcome::Ok(value) => Outcome::Ok(crate::people::name_people(services, value).await), | |
| 173 | + | failed => failed, | |
| 174 | + | }) | |
| 171 | 175 | } | |
| 172 | 176 | ||
| 173 | 177 | /// Appends the entry, if this is something the log keeps. A failure to |
| 21 | 21 | mod oauth; | |
| 22 | 22 | mod oidc; | |
| 23 | 23 | mod packages; | |
| 24 | + | mod people; | |
| 24 | 25 | mod openapi; | |
| 25 | 26 | mod pins; | |
| 26 | 27 | mod projects; | |
| ⋯ | |||
| 276 | 277 | DeviceClaim::Approved { token, user } => json!({ | |
| 277 | 278 | "status": "approved", | |
| 278 | 279 | "token": token, | |
| 280 | + | "display_username": user.display_username.clone().filter(|display| display.eq_ignore_ascii_case(&user.username)).unwrap_or_else(|| user.username.clone()), | |
| 279 | 281 | "username": user.username, | |
| 280 | 282 | "verified": user.verified, | |
| 281 | 283 | }), | |
| 1048 | 1048 | "properties": { | |
| 1049 | 1049 | "status": { "type": "string", "enum": ["pending", "approved", "denied", "expired"] }, | |
| 1050 | 1050 | "token": { "type": "string", "description": "Present when approved." }, | |
| 1051 | − | "username": { "type": "string" }, | |
| 1051 | + | "username": { "type": "string", "description": "Lowercased: what the account is found and linked by." }, | |
| 1052 | + | "display_username": { "type": "string", "description": "The username as its owner wrote it; the same as username when they chose no case." }, | |
| 1052 | 1053 | "verified": { | |
| 1053 | 1054 | "type": "boolean", | |
| 1054 | 1055 | "description": "Whether the account's email is confirmed.", |
| 1277 | 1277 | "Change a workspace's display name and description, what every member gets on each of its repositories (base_permission: none, read, write or admin), who may create its teams (team_creation: members or owners), its member privileges, and whether it requires two-factor authentication. The member privileges are: members_can_create_public_repositories and members_can_create_private_repositories (who may create each kind; owners always can), members_can_change_repo_visibility (members with the Admin role on a repository may make it public or private), members_can_delete_repositories (they may delete or transfer it) and members_can_invite_outside_collaborators (they may give a role to someone outside the workspace). two_factor_requirement_enabled true holds every member and outside collaborator without two-factor authentication out of the workspace until they turn it on; you need it on yourself first. Only the fields given are changed; give at least one. An empty name falls back to the slug, which this never changes (that is a rename, on Settings); an empty description clears it. Owners only, signed in as a person. Returns the workspace as it is now." | |
| 1278 | 1278 | } | |
| 1279 | 1279 | Op::ListMembers => { | |
| 1280 | − | "A workspace's members, owners first, then by username. Each has their `username`, `name`, `avatar`, `role` (`owner` or `member`), the roles they hold besides it (`org_roles`: `billing_manager`, `security_manager`), and, when an owner asks, whether they have two-factor authentication on (`two_factor`; null for anyone else). Members only." | |
| 1280 | + | "A workspace's members, owners first, then by username. Each has their `username`, `display_username` (the username as they wrote it), `name`, `avatar`, `role` (`owner` or `member`), the roles they hold besides it (`org_roles`: `billing_manager`, `security_manager`), and, when an owner asks, whether they have two-factor authentication on (`two_factor`; null for anyone else). Members only." | |
| 1281 | 1281 | } | |
| 1282 | 1282 | Op::UpdateMember => { | |
| 1283 | 1283 | "Change a member's role in a workspace: `role` (`owner` or `member`) and the roles they hold besides it (`org_roles`, a list of `billing_manager` and `security_manager`, which replaces the one they have). Only the fields given are changed. A billing manager manages the workspace's billing as an owner does, and gets nothing on repositories from it; a security manager reads every repository and sees and manages its security alerts and security settings. Refused with `409` when it would leave the workspace without an owner. Owners only, signed in as a person. Returns the member." | |
| ⋯ | |||
| 1369 | 1369 | "Issues on a repository, newest first. An issue is something that should change: a bug, a feature, a question. Pull requests are made against it. Filter by state, by a label's name, or by a milestone's number." | |
| 1370 | 1370 | } | |
| 1371 | 1371 | Op::GetIssue => { | |
| 1372 | − | "An issue: its description (which may say what done means, under \"Definition of done\"), labels, its comments, and every pull request made against it with its status. If the issue is closed, resolved_by is the number of the pull request that was merged for it. Read this before opening a pull request, to see what others have already tried." | |
| 1372 | + | "An issue: its description (which may say what done means, under \"Definition of done\"), labels, its comments, and every pull request made against it with its status. If the issue is closed, resolved_by is the number of the pull request that was merged for it. Read this before opening a pull request, to see what others have already tried. A comment one of the workspace's agents wrote as itself has `agent` (its `id`, `handle`, `display_name` and `avatar_seed`) and `acting_for` (the person it acted for, whose access capped it); its `author` is the agent, of kind `agent`." | |
| 1373 | 1373 | } | |
| 1374 | 1374 | Op::CreateIssue => { | |
| 1375 | 1375 | "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. labels are the repository's labels by name; a name it does not have yet is created when you have the Triage role or higher, and refused otherwise. milestone, a milestone's number, needs the Triage role." | |
| ⋯ | |||
| 1452 | 1452 | "Pull requests on a repository, newest first. State open covers drafts and those ready for review; closed covers merged and closed. Filter by a label's name, a milestone's number, or base, the branch they merge into." | |
| 1453 | 1453 | } | |
| 1454 | 1454 | Op::GetPullRequest => { | |
| 1455 | − | "A pull request's status, base (the branch it merges into), head commit, labels, milestone, comments and reviews, the issue it is for, its checks (statuses: what each workflow run reported on its head, with a link to the run; get_workflow_run and get_job_logs say why one failed), required_checks (each check the rules of the branch it merges into require, as success, failure, pending or expected when nothing has reported it yet), rules (each rule of that branch it does not meet yet, with the ruleset it comes from, what is wrong and how to meet it, in `unmet`; those you may bypass in `bypassable`; those of rulesets in evaluate that would refuse it in `evaluate`; and whether merging joins the merge queue), whether it is behind the branch it would merge into, and overlaps: other pull requests in progress that change the same files. An overlap with a pull request for a different issue means the two will conflict; say so, or keep clear of those files. `pull.reviewers` lists the people asked to review it and `pull.team_reviewers` the teams, as `workspace/team`. `code_owners` is there when the branch it merges into has a CODEOWNERS file: its `path`, whether code owners' approval is `required`, `reviews` (one per section and rule that owns a changed file, with its `section`, `line`, `pattern`, `owners`, `files`, whether it is `optional`, the approvals `required`, who it was `approved_by` and `changes_requested_by`, and whether it is `satisfied`), what is still `missing`, and how many `errors` the file has (get_codeowners_errors lists them)." | |
| 1455 | + | "A pull request's status, base (the branch it merges into), head commit, labels, milestone, comments and reviews, the issue it is for, its checks (statuses: what each workflow run reported on its head, with a link to the run; get_workflow_run and get_job_logs say why one failed), required_checks (each check the rules of the branch it merges into require, as success, failure, pending or expected when nothing has reported it yet), rules (each rule of that branch it does not meet yet, with the ruleset it comes from, what is wrong and how to meet it, in `unmet`; those you may bypass in `bypassable`; those of rulesets in evaluate that would refuse it in `evaluate`; and whether merging joins the merge queue), whether it is behind the branch it would merge into, and overlaps: other pull requests in progress that change the same files. An overlap with a pull request for a different issue means the two will conflict; say so, or keep clear of those files. `pull.reviewers` lists the people asked to review it and `pull.team_reviewers` the teams, as `workspace/team`. `code_owners` is there when the branch it merges into has a CODEOWNERS file: its `path`, whether code owners' approval is `required`, `reviews` (one per section and rule that owns a changed file, with its `section`, `line`, `pattern`, `owners`, `files`, whether it is `optional`, the approvals `required`, who it was `approved_by` and `changes_requested_by`, and whether it is `satisfied`), what is still `missing`, and how many `errors` the file has (get_codeowners_errors lists them). A comment one of the workspace's agents wrote as itself has `agent` (its `id`, `handle`, `display_name` and `avatar_seed`) and `acting_for` (the person it acted for, whose access capped it); its `author` is the agent, of kind `agent`. An agent's review also has `advisory: true`: its `verdict` (none, for a review that only comments) is shown but never counts toward required approvals or code owners, and never blocks a merge." | |
| 1456 | 1456 | } | |
| 1457 | 1457 | Op::CreatePullRequest => { | |
| 1458 | 1458 | "Start a change. Opens a draft pull request with its own fork of the repository and returns the fork's git remote. Clone it, commit your work there, push, record your session as you go, then call mark_pull_request_ready. Give the issue it is for whenever there is one. If the change is already on a branch pushed to the repository, give that branch instead: no fork is made and the pull request is ready for review at once. It merges into the default branch unless base names another existing branch; leave base out unless you were asked for another." | |
| 1 | + | //! Usernames as their owners wrote them. Every person in an answer, the | |
| 2 | + | //! REST API's and MCP's alike (an issue's `author`, a member, a | |
| 3 | + | //! collaborator, whoever `whoami` is), carries `display_username` beside | |
| 4 | + | //! `username`: `username` stays the lowercased key, stable for anything | |
| 5 | + | //! that matches on it, and `display_username` is the case its owner chose | |
| 6 | + | //! (`Ana`), the same as `username` when they chose none. | |
| 7 | + | //! | |
| 8 | + | //! The services keep the lowercased name; this fills the chosen case in on | |
| 9 | + | //! the way out, with one call to identity per answer. | |
| 10 | + | ||
| 11 | + | use std::collections::{BTreeSet, HashMap}; | |
| 12 | + | ||
| 13 | + | use g1t_contracts::identity::DisplayUsernamesArgs; | |
| 14 | + | use serde_json::Value; | |
| 15 | + | ||
| 16 | + | use crate::operations::Services; | |
| 17 | + | ||
| 18 | + | /// The most people one answer looks up; the rest show `username` as it is. | |
| 19 | + | const MOST: usize = 200; | |
| 20 | + | ||
| 21 | + | /// Whether `object` is a person (or another principal) named by `username`, | |
| 22 | + | /// rather than a set of credentials that happens to carry one. | |
| 23 | + | fn names_someone(object: &serde_json::Map<String, Value>) -> bool { | |
| 24 | + | matches!(object.get("username"), Some(Value::String(name)) if !name.is_empty()) | |
| 25 | + | && !object.contains_key("password") | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | /// The lowercased usernames in `value`, each once, at most [`MOST`]. | |
| 29 | + | pub fn usernames_in(value: &Value) -> Vec<String> { | |
| 30 | + | fn walk(value: &Value, out: &mut BTreeSet<String>) { | |
| 31 | + | match value { | |
| 32 | + | Value::Object(object) => { | |
| 33 | + | if names_someone(object) | |
| 34 | + | && let Some(Value::String(name)) = object.get("username") | |
| 35 | + | { | |
| 36 | + | out.insert(name.to_lowercase()); | |
| 37 | + | } | |
| 38 | + | object.values().for_each(|value| walk(value, out)); | |
| 39 | + | } | |
| 40 | + | Value::Array(items) => items.iter().for_each(|value| walk(value, out)), | |
| 41 | + | _ => {} | |
| 42 | + | } | |
| 43 | + | } | |
| 44 | + | let mut out = BTreeSet::new(); | |
| 45 | + | walk(value, &mut out); | |
| 46 | + | out.into_iter().take(MOST).collect() | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | /// Gives every person in `value` a `display_username`: the case from | |
| 50 | + | /// `chosen` (by lowercased username), one they already carry when it is | |
| 51 | + | /// the same name, or else `username` itself. | |
| 52 | + | pub fn fill(value: &mut Value, chosen: &HashMap<String, String>) { | |
| 53 | + | match value { | |
| 54 | + | Value::Object(object) => { | |
| 55 | + | if names_someone(object) { | |
| 56 | + | let username = object["username"].as_str().unwrap_or_default().to_owned(); | |
| 57 | + | // A service's own `displayUsername` (a profile's) becomes this. | |
| 58 | + | let theirs = object.remove("displayUsername"); | |
| 59 | + | let carried = object | |
| 60 | + | .get("display_username") | |
| 61 | + | .or(theirs.as_ref()) | |
| 62 | + | .and_then(Value::as_str) | |
| 63 | + | .filter(|display| display.eq_ignore_ascii_case(&username)) | |
| 64 | + | .map(str::to_owned); | |
| 65 | + | let shown = chosen | |
| 66 | + | .get(&username.to_lowercase()) | |
| 67 | + | .cloned() | |
| 68 | + | .or(carried) | |
| 69 | + | .unwrap_or(username); | |
| 70 | + | object.insert("display_username".to_owned(), Value::String(shown)); | |
| 71 | + | } | |
| 72 | + | object.values_mut().for_each(|value| fill(value, chosen)); | |
| 73 | + | } | |
| 74 | + | Value::Array(items) => items.iter_mut().for_each(|value| fill(value, chosen)), | |
| 75 | + | _ => {} | |
| 76 | + | } | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | /// `value` with each person's chosen case filled in. When identity does | |
| 80 | + | /// not answer, the people are still given `display_username`, as their | |
| 81 | + | /// `username`: an answer is never held up for it. | |
| 82 | + | pub async fn name_people(services: &Services, mut value: Value) -> Value { | |
| 83 | + | let usernames = usernames_in(&value); | |
| 84 | + | if usernames.is_empty() { | |
| 85 | + | return value; | |
| 86 | + | } | |
| 87 | + | let chosen: HashMap<String, String> = | |
| 88 | + | g1t_kit::call(&services.identity, "display_usernames", &DisplayUsernamesArgs { usernames }) | |
| 89 | + | .await | |
| 90 | + | .unwrap_or_default(); | |
| 91 | + | fill(&mut value, &chosen); | |
| 92 | + | value | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | #[cfg(test)] | |
| 96 | + | mod tests { | |
| 97 | + | use super::*; | |
| 98 | + | use serde_json::json; | |
| 99 | + | ||
| 100 | + | #[test] | |
| 101 | + | fn every_person_in_an_answer_is_found_once() { | |
| 102 | + | let answer = json!({ | |
| 103 | + | "issue": { "author": { "id": "usr_1", "username": "ana" }, "assignees": [{ "username": "bo" }, { "username": "ana" }] }, | |
| 104 | + | "git": { "username": "ana", "password": "your g1t access token" }, | |
| 105 | + | "count": 3, | |
| 106 | + | }); | |
| 107 | + | assert_eq!(usernames_in(&answer), vec!["ana".to_owned(), "bo".to_owned()]); | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | #[test] | |
| 111 | + | fn people_get_the_case_they_chose_or_their_username() { | |
| 112 | + | let mut answer = json!({ | |
| 113 | + | "author": { "id": "usr_1", "username": "ana" }, | |
| 114 | + | "members": [{ "username": "bo" }, { "username": "cy", "display_username": "Cy" }], | |
| 115 | + | "git": { "username": "ana", "password": "your g1t access token" }, | |
| 116 | + | }); | |
| 117 | + | let chosen = HashMap::from([("ana".to_owned(), "Ana".to_owned())]); | |
| 118 | + | fill(&mut answer, &chosen); | |
| 119 | + | assert_eq!(answer["author"]["username"], "ana"); | |
| 120 | + | assert_eq!(answer["author"]["display_username"], "Ana"); | |
| 121 | + | assert_eq!(answer["members"][0]["display_username"], "bo"); | |
| 122 | + | assert_eq!(answer["members"][1]["display_username"], "Cy"); | |
| 123 | + | assert!(answer["git"].get("display_username").is_none()); | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | #[test] | |
| 127 | + | fn a_carried_case_of_another_name_is_not_kept() { | |
| 128 | + | let mut answer = json!({ "username": "ana", "display_username": "Bob" }); | |
| 129 | + | fill(&mut answer, &HashMap::new()); | |
| 130 | + | assert_eq!(answer["display_username"], "ana"); | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | #[test] | |
| 134 | + | fn a_profiles_own_spelling_is_folded_in() { | |
| 135 | + | let mut answer = json!({ "username": "ana", "displayUsername": "Ana" }); | |
| 136 | + | fill(&mut answer, &HashMap::new()); | |
| 137 | + | assert_eq!(answer, json!({ "username": "ana", "display_username": "Ana" })); | |
| 138 | + | } | |
| 139 | + | } |
| 20 | 20 | "status": "approved", | |
| 21 | 21 | "token": "g1t_…", | |
| 22 | 22 | "username": "syntaqx", | |
| 23 | + | "display_username": "syntaqx", | |
| 23 | 24 | "verified": true | |
| 24 | 25 | } | |
| 25 | 26 | }, | |
| ⋯ | |||
| 27 | 28 | "response": { | |
| 28 | 29 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 29 | 30 | "username": "syntaqx", | |
| 31 | + | "display_username": "syntaqx", | |
| 30 | 32 | "kind": "user", | |
| 31 | 33 | "verified": true, | |
| 32 | 34 | "workspaces": [ | |
| ⋯ | |||
| 134 | 136 | "response": [ | |
| 135 | 137 | { | |
| 136 | 138 | "username": "ada", | |
| 139 | + | "display_username": "Ada", | |
| 137 | 140 | "role": "owner", | |
| 138 | 141 | "org_roles": [], | |
| 139 | 142 | "two_factor": true, | |
| ⋯ | |||
| 142 | 145 | }, | |
| 143 | 146 | { | |
| 144 | 147 | "username": "grace", | |
| 148 | + | "display_username": "grace", | |
| 145 | 149 | "role": "member", | |
| 146 | 150 | "org_roles": [ | |
| 147 | 151 | "security_manager" | |
| ⋯ | |||
| 166 | 170 | }, | |
| 167 | 171 | "response": { | |
| 168 | 172 | "username": "grace", | |
| 173 | + | "display_username": "grace", | |
| 169 | 174 | "role": "member", | |
| 170 | 175 | "org_roles": [ | |
| 171 | 176 | "security_manager" | |
| ⋯ | |||
| 290 | 295 | }, | |
| 291 | 296 | "response": { | |
| 292 | 297 | "username": "ada", | |
| 298 | + | "display_username": "Ada", | |
| 293 | 299 | "email": "ada@example.com", | |
| 294 | 300 | "verified": true, | |
| 295 | 301 | "joined": null, | |
| ⋯ | |||
| 578 | 584 | "role": "member", | |
| 579 | 585 | "invited_by": { | |
| 580 | 586 | "username": "syntaqx", | |
| 587 | + | "display_username": "syntaqx", | |
| 581 | 588 | "name": "Chase", | |
| 582 | 589 | "avatar": null | |
| 583 | 590 | }, | |
| ⋯ | |||
| 1053 | 1060 | "author": { | |
| 1054 | 1061 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1055 | 1062 | "username": "syntaqx", | |
| 1063 | + | "display_username": "syntaqx", | |
| 1056 | 1064 | "kind": "user", | |
| 1057 | 1065 | "verified": false, | |
| 1058 | 1066 | "workspaces": [] | |
| ⋯ | |||
| 1095 | 1103 | "author": { | |
| 1096 | 1104 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1097 | 1105 | "username": "syntaqx", | |
| 1106 | + | "display_username": "syntaqx", | |
| 1098 | 1107 | "kind": "user", | |
| 1099 | 1108 | "verified": false, | |
| 1100 | 1109 | "workspaces": [] | |
| ⋯ | |||
| 1130 | 1139 | "author": { | |
| 1131 | 1140 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1132 | 1141 | "username": "syntaqx", | |
| 1142 | + | "display_username": "syntaqx", | |
| 1133 | 1143 | "kind": "user", | |
| 1134 | 1144 | "verified": false, | |
| 1135 | 1145 | "workspaces": [] | |
| ⋯ | |||
| 1178 | 1188 | "author": { | |
| 1179 | 1189 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1180 | 1190 | "username": "syntaqx", | |
| 1191 | + | "display_username": "syntaqx", | |
| 1181 | 1192 | "kind": "user", | |
| 1182 | 1193 | "verified": false, | |
| 1183 | 1194 | "workspaces": [] | |
| ⋯ | |||
| 1225 | 1236 | "author": { | |
| 1226 | 1237 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1227 | 1238 | "username": "syntaqx", | |
| 1239 | + | "display_username": "syntaqx", | |
| 1228 | 1240 | "kind": "user", | |
| 1229 | 1241 | "verified": false, | |
| 1230 | 1242 | "workspaces": [] | |
| ⋯ | |||
| 1241 | 1253 | "author": { | |
| 1242 | 1254 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1243 | 1255 | "username": "syntaqx", | |
| 1256 | + | "display_username": "syntaqx", | |
| 1244 | 1257 | "kind": "user", | |
| 1245 | 1258 | "verified": false, | |
| 1246 | 1259 | "workspaces": [] | |
| ⋯ | |||
| 1282 | 1295 | "author": { | |
| 1283 | 1296 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1284 | 1297 | "username": "syntaqx", | |
| 1298 | + | "display_username": "syntaqx", | |
| 1285 | 1299 | "kind": "user", | |
| 1286 | 1300 | "verified": false, | |
| 1287 | 1301 | "workspaces": [] | |
| ⋯ | |||
| 1323 | 1337 | "author": { | |
| 1324 | 1338 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1325 | 1339 | "username": "syntaqx", | |
| 1340 | + | "display_username": "syntaqx", | |
| 1326 | 1341 | "kind": "user", | |
| 1327 | 1342 | "verified": false, | |
| 1328 | 1343 | "workspaces": [] | |
| ⋯ | |||
| 1356 | 1371 | "author": { | |
| 1357 | 1372 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1358 | 1373 | "username": "syntaqx", | |
| 1374 | + | "display_username": "syntaqx", | |
| 1359 | 1375 | "kind": "user", | |
| 1360 | 1376 | "verified": false, | |
| 1361 | 1377 | "workspaces": [] | |
| ⋯ | |||
| 1408 | 1424 | "author": { | |
| 1409 | 1425 | "id": "usr_g1t_agent", | |
| 1410 | 1426 | "username": "g1t", | |
| 1427 | + | "display_username": "g1t", | |
| 1411 | 1428 | "kind": "agent", | |
| 1412 | 1429 | "verified": false, | |
| 1413 | 1430 | "workspaces": [] | |
| ⋯ | |||
| 1415 | 1432 | "requested_by": { | |
| 1416 | 1433 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1417 | 1434 | "username": "syntaqx", | |
| 1435 | + | "display_username": "syntaqx", | |
| 1418 | 1436 | "kind": "user", | |
| 1419 | 1437 | "verified": false, | |
| 1420 | 1438 | "workspaces": [] | |
| ⋯ | |||
| 1444 | 1462 | "author": { | |
| 1445 | 1463 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1446 | 1464 | "username": "syntaqx", | |
| 1465 | + | "display_username": "syntaqx", | |
| 1447 | 1466 | "kind": "user", | |
| 1448 | 1467 | "verified": false, | |
| 1449 | 1468 | "workspaces": [] | |
| ⋯ | |||
| 1491 | 1510 | "author": { | |
| 1492 | 1511 | "id": "usr_g1t_agent", | |
| 1493 | 1512 | "username": "g1t", | |
| 1513 | + | "display_username": "g1t", | |
| 1494 | 1514 | "kind": "agent", | |
| 1495 | 1515 | "verified": false, | |
| 1496 | 1516 | "workspaces": [] | |
| ⋯ | |||
| 1498 | 1518 | "requested_by": { | |
| 1499 | 1519 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1500 | 1520 | "username": "syntaqx", | |
| 1521 | + | "display_username": "syntaqx", | |
| 1501 | 1522 | "kind": "user", | |
| 1502 | 1523 | "verified": false, | |
| 1503 | 1524 | "workspaces": [] | |
| ⋯ | |||
| 1525 | 1546 | "author": { | |
| 1526 | 1547 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1527 | 1548 | "username": "syntaqx", | |
| 1549 | + | "display_username": "syntaqx", | |
| 1528 | 1550 | "kind": "user", | |
| 1529 | 1551 | "verified": true, | |
| 1530 | 1552 | "workspaces": [ | |
| ⋯ | |||
| 1554 | 1576 | "author": { | |
| 1555 | 1577 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1556 | 1578 | "username": "syntaqx", | |
| 1579 | + | "display_username": "syntaqx", | |
| 1557 | 1580 | "kind": "user", | |
| 1558 | 1581 | "verified": true, | |
| 1559 | 1582 | "workspaces": [ | |
| ⋯ | |||
| 1660 | 1683 | "author": { | |
| 1661 | 1684 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1662 | 1685 | "username": "syntaqx", | |
| 1686 | + | "display_username": "syntaqx", | |
| 1663 | 1687 | "kind": "user", | |
| 1664 | 1688 | "verified": false, | |
| 1665 | 1689 | "workspaces": [] | |
| ⋯ | |||
| 1722 | 1746 | "author": { | |
| 1723 | 1747 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1724 | 1748 | "username": "syntaqx", | |
| 1749 | + | "display_username": "syntaqx", | |
| 1725 | 1750 | "kind": "user", | |
| 1726 | 1751 | "verified": false, | |
| 1727 | 1752 | "workspaces": [] | |
| ⋯ | |||
| 1775 | 1800 | "author": { | |
| 1776 | 1801 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1777 | 1802 | "username": "syntaqx", | |
| 1803 | + | "display_username": "syntaqx", | |
| 1778 | 1804 | "kind": "user", | |
| 1779 | 1805 | "verified": false, | |
| 1780 | 1806 | "workspaces": [] | |
| ⋯ | |||
| 1826 | 1852 | "author": { | |
| 1827 | 1853 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1828 | 1854 | "username": "syntaqx", | |
| 1855 | + | "display_username": "syntaqx", | |
| 1829 | 1856 | "kind": "user", | |
| 1830 | 1857 | "verified": false, | |
| 1831 | 1858 | "workspaces": [] | |
| ⋯ | |||
| 1883 | 1910 | "author": { | |
| 1884 | 1911 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1885 | 1912 | "username": "syntaqx", | |
| 1913 | + | "display_username": "syntaqx", | |
| 1886 | 1914 | "kind": "user", | |
| 1887 | 1915 | "verified": false, | |
| 1888 | 1916 | "workspaces": [] | |
| ⋯ | |||
| 1910 | 1938 | "author": { | |
| 1911 | 1939 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1912 | 1940 | "username": "syntaqx", | |
| 1941 | + | "display_username": "syntaqx", | |
| 1913 | 1942 | "kind": "user", | |
| 1914 | 1943 | "verified": false, | |
| 1915 | 1944 | "workspaces": [] | |
| ⋯ | |||
| 1933 | 1962 | "author": { | |
| 1934 | 1963 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1935 | 1964 | "username": "syntaqx", | |
| 1965 | + | "display_username": "syntaqx", | |
| 1936 | 1966 | "kind": "user", | |
| 1937 | 1967 | "verified": false, | |
| 1938 | 1968 | "workspaces": [] | |
| ⋯ | |||
| 1942 | 1972 | "line": null, | |
| 1943 | 1973 | "verdict": null, | |
| 1944 | 1974 | "created_at": "2026-10-01T18:33:10.420Z" | |
| 1975 | + | }, | |
| 1976 | + | { | |
| 1977 | + | "id": "cmt_01m43sw2a6c0e4h8k2n6r0v4z8", | |
| 1978 | + | "kind": "comment", | |
| 1979 | + | "author": { | |
| 1980 | + | "id": "agt_01m43q8d2f6h0k4n8r2v6z0c4g", | |
| 1981 | + | "username": "margo", | |
| 1982 | + | "kind": "agent", | |
| 1983 | + | "verified": false, | |
| 1984 | + | "workspaces": [] | |
| 1985 | + | }, | |
| 1986 | + | "body": "The empty-name case is handled and tested. One nit: trim the name before using it.", | |
| 1987 | + | "path": null, | |
| 1988 | + | "line": null, | |
| 1989 | + | "verdict": "approve", | |
| 1990 | + | "created_at": "2026-10-01T18:36:41.207Z", | |
| 1991 | + | "agent": { | |
| 1992 | + | "id": "agt_01m43q8d2f6h0k4n8r2v6z0c4g", | |
| 1993 | + | "handle": "margo", | |
| 1994 | + | "display_name": "Margo", | |
| 1995 | + | "avatar_seed": "margo" | |
| 1996 | + | }, | |
| 1997 | + | "acting_for": { | |
| 1998 | + | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1999 | + | "username": "syntaqx", | |
| 2000 | + | "kind": "user", | |
| 2001 | + | "verified": false, | |
| 2002 | + | "workspaces": [] | |
| 2003 | + | }, | |
| 2004 | + | "advisory": true | |
| 1945 | 2005 | } | |
| 1946 | 2006 | ], | |
| 1947 | 2007 | "checks": null, | |
| ⋯ | |||
| 2097 | 2157 | "author": { | |
| 2098 | 2158 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2099 | 2159 | "username": "syntaqx", | |
| 2160 | + | "display_username": "syntaqx", | |
| 2100 | 2161 | "kind": "user", | |
| 2101 | 2162 | "verified": false, | |
| 2102 | 2163 | "workspaces": [] | |
| ⋯ | |||
| 2117 | 2178 | "author": { | |
| 2118 | 2179 | "id": "usr_01kz9d3f7h1k5n9r3v7z1c5g9b", | |
| 2119 | 2180 | "username": "ana", | |
| 2181 | + | "display_username": "Ana", | |
| 2120 | 2182 | "kind": "user", | |
| 2121 | 2183 | "verified": true, | |
| 2122 | 2184 | "workspaces": [ | |
| ⋯ | |||
| 2187 | 2249 | "author": { | |
| 2188 | 2250 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2189 | 2251 | "username": "syntaqx", | |
| 2252 | + | "display_username": "syntaqx", | |
| 2190 | 2253 | "kind": "user", | |
| 2191 | 2254 | "verified": false, | |
| 2192 | 2255 | "workspaces": [] | |
| ⋯ | |||
| 2248 | 2311 | "author": { | |
| 2249 | 2312 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2250 | 2313 | "username": "syntaqx", | |
| 2314 | + | "display_username": "syntaqx", | |
| 2251 | 2315 | "kind": "user", | |
| 2252 | 2316 | "verified": false, | |
| 2253 | 2317 | "workspaces": [] | |
| ⋯ | |||
| 2301 | 2365 | "author": { | |
| 2302 | 2366 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2303 | 2367 | "username": "syntaqx", | |
| 2368 | + | "display_username": "syntaqx", | |
| 2304 | 2369 | "kind": "user", | |
| 2305 | 2370 | "verified": false, | |
| 2306 | 2371 | "workspaces": [] | |
| ⋯ | |||
| 2349 | 2414 | "author": { | |
| 2350 | 2415 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2351 | 2416 | "username": "syntaqx", | |
| 2417 | + | "display_username": "syntaqx", | |
| 2352 | 2418 | "kind": "user", | |
| 2353 | 2419 | "verified": false, | |
| 2354 | 2420 | "workspaces": [] | |
| ⋯ | |||
| 2396 | 2462 | "author": { | |
| 2397 | 2463 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2398 | 2464 | "username": "syntaqx", | |
| 2465 | + | "display_username": "syntaqx", | |
| 2399 | 2466 | "kind": "user", | |
| 2400 | 2467 | "verified": false, | |
| 2401 | 2468 | "workspaces": [] | |
| ⋯ | |||
| 2444 | 2511 | "author": { | |
| 2445 | 2512 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2446 | 2513 | "username": "syntaqx", | |
| 2514 | + | "display_username": "syntaqx", | |
| 2447 | 2515 | "kind": "user", | |
| 2448 | 2516 | "verified": false, | |
| 2449 | 2517 | "workspaces": [] | |
| ⋯ | |||
| 4506 | 4574 | "people": [ | |
| 4507 | 4575 | { | |
| 4508 | 4576 | "username": "syntaqx", | |
| 4577 | + | "display_username": "syntaqx", | |
| 4509 | 4578 | "name": "Chase Pierce", | |
| 4510 | 4579 | "avatar": null, | |
| 4511 | 4580 | "role": "admin", | |
| ⋯ | |||
| 4515 | 4584 | }, | |
| 4516 | 4585 | { | |
| 4517 | 4586 | "username": "linus", | |
| 4587 | + | "display_username": "linus", | |
| 4518 | 4588 | "name": null, | |
| 4519 | 4589 | "avatar": null, | |
| 4520 | 4590 | "role": "maintain", | |
| ⋯ | |||
| 4524 | 4594 | }, | |
| 4525 | 4595 | { | |
| 4526 | 4596 | "username": "grace", | |
| 4597 | + | "display_username": "grace", | |
| 4527 | 4598 | "name": "Grace Hopper", | |
| 4528 | 4599 | "avatar": null, | |
| 4529 | 4600 | "role": "write", | |
| ⋯ | |||
| 4533 | 4604 | }, | |
| 4534 | 4605 | { | |
| 4535 | 4606 | "username": "ada", | |
| 4607 | + | "display_username": "Ada", | |
| 4536 | 4608 | "name": "Ada Lovelace", | |
| 4537 | 4609 | "avatar": null, | |
| 4538 | 4610 | "role": "triage", | |
| ⋯ | |||
| 4608 | 4680 | }, | |
| 4609 | 4681 | "response": { | |
| 4610 | 4682 | "username": "ada", | |
| 4683 | + | "display_username": "Ada", | |
| 4611 | 4684 | "name": "Ada Lovelace", | |
| 4612 | 4685 | "avatar": null, | |
| 4613 | 4686 | "role": "write", | |
| ⋯ | |||
| 4634 | 4707 | }, | |
| 4635 | 4708 | "response": { | |
| 4636 | 4709 | "username": "ada", | |
| 4710 | + | "display_username": "Ada", | |
| 4637 | 4711 | "role": "triage", | |
| 4638 | 4712 | "source": "direct", | |
| 4639 | 4713 | "capabilities": [ | |
| ⋯ | |||
| 4772 | 4846 | "response": [ | |
| 4773 | 4847 | { | |
| 4774 | 4848 | "username": "ada", | |
| 4849 | + | "display_username": "Ada", | |
| 4775 | 4850 | "name": "Ada Lovelace", | |
| 4776 | 4851 | "avatar": null, | |
| 4777 | 4852 | "repos": [ | |
| ⋯ | |||
| 5047 | 5122 | "response": [ | |
| 5048 | 5123 | { | |
| 5049 | 5124 | "username": "syntaqx", | |
| 5125 | + | "display_username": "syntaqx", | |
| 5050 | 5126 | "name": "Chase Pierce", | |
| 5051 | 5127 | "avatar": null, | |
| 5052 | 5128 | "role": "maintainer", | |
| ⋯ | |||
| 5054 | 5130 | }, | |
| 5055 | 5131 | { | |
| 5056 | 5132 | "username": "ana", | |
| 5133 | + | "display_username": "Ana", | |
| 5057 | 5134 | "name": "Ana Lima", | |
| 5058 | 5135 | "avatar": null, | |
| 5059 | 5136 | "role": "member", | |
| ⋯ | |||
| 5061 | 5138 | }, | |
| 5062 | 5139 | { | |
| 5063 | 5140 | "username": "bo", | |
| 5141 | + | "display_username": "bo", | |
| 5064 | 5142 | "name": null, | |
| 5065 | 5143 | "avatar": null, | |
| 5066 | 5144 | "role": "member", | |
| ⋯ | |||
| 5080 | 5158 | }, | |
| 5081 | 5159 | "response": { | |
| 5082 | 5160 | "username": "ana", | |
| 5161 | + | "display_username": "Ana", | |
| 5083 | 5162 | "name": "Ana Lima", | |
| 5084 | 5163 | "avatar": null, | |
| 5085 | 5164 | "role": "maintainer", | |
| ⋯ | |||
| 8648 | 8727 | "author": { | |
| 8649 | 8728 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 8650 | 8729 | "username": "syntaqx", | |
| 8730 | + | "display_username": "syntaqx", | |
| 8651 | 8731 | "kind": "user", | |
| 8652 | 8732 | "verified": false, | |
| 8653 | 8733 | "workspaces": [] | |
| ⋯ | |||
| 8713 | 8793 | "author": { | |
| 8714 | 8794 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 8715 | 8795 | "username": "syntaqx", | |
| 8796 | + | "display_username": "syntaqx", | |
| 8716 | 8797 | "kind": "user", | |
| 8717 | 8798 | "verified": false, | |
| 8718 | 8799 | "workspaces": [] | |
| ⋯ | |||
| 8773 | 8854 | "author": { | |
| 8774 | 8855 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 8775 | 8856 | "username": "syntaqx", | |
| 8857 | + | "display_username": "syntaqx", | |
| 8776 | 8858 | "kind": "user", | |
| 8777 | 8859 | "verified": false, | |
| 8778 | 8860 | "workspaces": [] | |
| ⋯ | |||
| 10849 | 10931 | "kind": "user", | |
| 10850 | 10932 | "name": "ada", | |
| 10851 | 10933 | "username": "ada", | |
| 10934 | + | "display_username": "Ada", | |
| 10852 | 10935 | "avatar": "5f2b8c1d9e7a3f6b4c0d2e8a1b9c7d5e3f1a0b2c4d6e8f0a1b3c5d7e9f0a2b4c", | |
| 10853 | 10936 | "commits": 52, | |
| 10854 | 10937 | "first_at": "2026-09-28T14:11:52.000Z", | |
| ⋯ | |||
| 10907 | 10990 | "response": [ | |
| 10908 | 10991 | { | |
| 10909 | 10992 | "username": "ada", | |
| 10993 | + | "display_username": "Ada", | |
| 10910 | 10994 | "avatar": null, | |
| 10911 | 10995 | "starred_at": "2026-10-06T18:30:00.000Z" | |
| 10912 | 10996 | }, | |
| 10913 | 10997 | { | |
| 10914 | 10998 | "username": "sam", | |
| 10999 | + | "display_username": "sam", | |
| 10915 | 11000 | "avatar": null, | |
| 10916 | 11001 | "starred_at": "2026-10-02T09:12:00.000Z" | |
| 10917 | 11002 | } | |
| 268 | 268 | assert!(wire::camel_case_keys(example).is_empty(), "{method} {path}"); | |
| 269 | 269 | continue; | |
| 270 | 270 | }; | |
| 271 | − | let sample = sample(op, example); | |
| 271 | + | let mut sample = sample(op, example); | |
| 272 | + | // Each person gets their chosen case on the way out (people.rs). | |
| 273 | + | crate::people::fill(&mut sample, &std::collections::HashMap::new()); | |
| 272 | 274 | converted += wire::camel_case_keys(&sample).len(); | |
| 273 | 275 | let sent = wire::snake_case(sample); | |
| 274 | 276 | let leaked = wire::camel_case_keys(&sent); |
| 436 | 436 | provider, the same reply costs $0.0008 at g1t, and your provider bills the | |
| 437 | 437 | model. | |
| 438 | 438 | ||
| 439 | + | ## On issues and pull requests | |
| 440 | + | ||
| 441 | + | An agent comments on issues and pull requests, and reviews pull requests, | |
| 442 | + | as itself: its face, its name with an **Agent** badge, and **on behalf of | |
| 443 | + | @person**, the person it was working for. It never does more there than | |
| 444 | + | that person could: they must be able to read the repository and comment | |
| 445 | + | on it. | |
| 446 | + | ||
| 447 | + | Its reviews are **advisory**. The verdict shows, marked **Advisory**, but | |
| 448 | + | it never counts toward the approvals a branch requires or a code owner's | |
| 449 | + | approval, and a request for changes from it never blocks a merge. A person | |
| 450 | + | still approves. An agent doesn't review drafts, and writes at most 5 | |
| 451 | + | comments and reviews on one issue or pull request an hour. See | |
| 452 | + | [agent reviews](/guides/pull-requests/#agent-reviews). | |
| 453 | + | ||
| 439 | 454 | ## What it may do alone | |
| 440 | 455 | ||
| 441 | 456 | Each agent says what it may do by itself and what needs a person first. |
| 72 | 72 | more hop in a chain that Priya started. Chains stop after six hops and | |
| 73 | 73 | hand back to a person, so two agents can't talk to each other forever. | |
| 74 | 74 | ||
| 75 | + | Everyone is shown by name. A person is their display name, or their | |
| 76 | + | username as they wrote it when they have none. An agent is its name with a | |
| 77 | + | small **Agent** tag. Their username or handle is on their card: select a | |
| 78 | + | name or avatar to see it. Messages someone sends within five minutes of | |
| 79 | + | each other sit under one name, and each new day starts with a divider. | |
| 80 | + | The sidebar, the people typing, notifications and phone alerts all name | |
| 81 | + | people the same way. | |
| 82 | + | ||
| 75 | 83 | <Aside type="note" title="What a reply can see today"> | |
| 76 | 84 | A reply reads the conversation it is in, and looks things up while it | |
| 77 | 85 | answers: code, issues, pull requests and earlier messages. It only reads | |
| ⋯ | |||
| 178 | 186 | ## Mentions | |
| 179 | 187 | ||
| 180 | 188 | Type `@` and a name to mention a person by username or an agent by handle: | |
| 181 | − | `@priya`, `@margo`. The sidebar counts mentions of you separately from other | |
| 182 | − | unread messages, so they stand out. | |
| 189 | + | `@priya`, `@margo`. Suggestions match display names too, so `@Pri` finds | |
| 190 | + | Priya Nair. In the conversation the mention reads as the name people know, | |
| 191 | + | `@Priya Nair`, and opens their card; the message itself keeps `@priya`. | |
| 192 | + | The sidebar counts mentions of you separately from other unread messages, | |
| 193 | + | so they stand out. | |
| 183 | 194 | ||
| 184 | 195 | | You mention | What happens | | |
| 185 | 196 | | --- | --- | | |
| ⋯ | |||
| 210 | 221 | When an agent starts a longer piece of work, it posts a **session card**: | |
| 211 | 222 | the session's title, a line such as *Step 3 · 12 tools · $0.14*, and its | |
| 212 | 223 | state. The card changes in place as the session moves, for everyone looking, | |
| 213 | − | and opens the session's page in Agents. | |
| 224 | + | and opens the session's page in Agents. Its buttons let you message the | |
| 225 | + | session, stop it or approve more spend without leaving the conversation; | |
| 226 | + | see [cards you can act on](#cards-you-can-act-on). | |
| 214 | 227 | ||
| 215 | 228 | | State | Means | | |
| 216 | 229 | | --- | --- | | |
| ⋯ | |||
| 222 | 235 | | **Stopped** | Someone stopped it. | | |
| 223 | 236 | | **Failed** | It could not finish. Shown in red. | | |
| 224 | 237 | ||
| 238 | + | ## Cards you can act on | |
| 239 | + | ||
| 240 | + | What an agent posts in chat is something you can act on where you read it. | |
| 241 | + | A card has a title, its state, often a preview and a few labelled facts | |
| 242 | + | (such as **Repository** and **Labels**), and a row of buttons. Buttons that | |
| 243 | + | open a place, such as **Open**, take you there. The others do the thing | |
| 244 | + | right in the conversation: | |
| 245 | + | ||
| 246 | + | - A button that can't be undone, such as **Stop**, asks you first. | |
| 247 | + | - A button that needs something from you opens a field under the card: an | |
| 248 | + | amount in dollars for **Approve more**, already filled in with a | |
| 249 | + | suggestion, or a line of text for **Message** and **Follow up**. Press | |
| 250 | + | Enter to send, Shift+Enter for a new line, and Esc to put the field away. | |
| 251 | + | - While it works, the button spins and the card's other buttons wait. A | |
| 252 | + | note then says what happened, such as *Approved up to $4.00. It's going | |
| 253 | + | on.*, or why it didn't. | |
| 254 | + | - The card itself changes in place, for everyone in the conversation, once | |
| 255 | + | the work is done: a filed draft becomes **Filed** with a link to the | |
| 256 | + | issue, a stopped session reads **Stopped**. | |
| 257 | + | ||
| 258 | + | A long preview shows its first few lines; choose **Show more** to read the | |
| 259 | + | rest. Cards work the same in a thread and on a phone. | |
| 260 | + | ||
| 261 | + | | Card | Its buttons | | |
| 262 | + | | --- | --- | | |
| 263 | + | | A session that is working | **Message** (it reads it at its next step), **Stop**, **Open** | | |
| 264 | + | | A session that needs approval | **Approve more** (type the new cap), **Stop**, **Open** | | |
| 265 | + | | A session that is done | Its report as the preview, **Follow up** (it picks up again with what it already knows), **Open** | | |
| 266 | + | | A draft issue | The issue as it would be filed, its repository and labels, **File issue**, **Discard** | | |
| 267 | + | | A filed issue | **Open issue** | | |
| 268 | + | ||
| 269 | + | ### Who can press what | |
| 270 | + | ||
| 271 | + | Anyone who can read the conversation sees the same buttons. What happens | |
| 272 | + | when you press one depends on who you are: | |
| 273 | + | ||
| 274 | + | | Button | Who can | | |
| 275 | + | | --- | --- | | |
| 276 | + | | **Message**, **Follow up** | Anyone who can read the conversation. | | |
| 277 | + | | **Stop** | Anyone who can read the conversation. | | |
| 278 | + | | **Approve more** | The workspace's owners. The new cap must be more than the session has already spent. | | |
| 279 | + | | **File issue** | Anyone who can read the repository. The issue is filed as you, with a line saying which agent drafted it. | | |
| 280 | + | | **Discard** | Whoever asked the agent for the draft, or a workspace owner. | | |
| 281 | + | ||
| 282 | + | If two people press at once, the first one wins and the second is told | |
| 283 | + | someone got there first. Agents never file, approve or stop anything | |
| 284 | + | through a card on their own: a person always presses the button. | |
| 285 | + | ||
| 225 | 286 | ## Live | |
| 226 | 287 | ||
| 227 | 288 | Chat is live. Messages, edits and deletions appear for everyone in the | |
| 83 | 83 | | Someone mentioned you with `@username` in a comment | You | `mention` | Info | | |
| 84 | 84 | | Someone mentioned a team with `@workspace/team` in a comment, or in an issue or pull request they opened | Everyone in the team and its child teams, when the team's notifications are on and the writer can see the team | `team_mention` | Info | | |
| 85 | 85 | | Someone commented on an issue or pull request | Everyone subscribed to it, and people watching its kind | Why each is subscribed, or `subscribed` for watchers | Info, or Success for an approval | | |
| 86 | + | | One of your workspace's agents commented on or reviewed an issue or pull request, as itself | The same people, and the person the pull request belongs to for a review; shown as from "Margo (agent)", with "(advisory)" on a review. See [agent reviews](/guides/pull-requests/#agent-reviews) | Why each is subscribed, or `author` for a review | Info, or Success for an approval | | |
| 86 | 87 | | An issue or pull request was opened | People watching its kind | `subscribed` | Info | | |
| 87 | 88 | ||
| 88 | 89 | "The person a pull request belongs to" is its author, or, for a change g1t |
| 176 | 176 | the MCP server they are the `pull_request` tool's `request_reviewers` and | |
| 177 | 177 | `remove_requested_reviewers` actions. | |
| 178 | 178 | ||
| 179 | + | ## Agent reviews | |
| 180 | + | ||
| 181 | + | Your workspace's [agents](/guides/agents/) comment on issues and pull | |
| 182 | + | requests, and review pull requests, as themselves: a comment from Margo | |
| 183 | + | shows her pixel face, her name with an **Agent** badge (linked to | |
| 184 | + | [her page](/guides/agents/#the-agents-page)), and **on behalf of @ana**, | |
| 185 | + | the person she was working for. | |
| 186 | + | ||
| 187 | + | An agent's review is **advisory**. It shows its verdict (approved, | |
| 188 | + | requested changes, or reviewed, for one that only comments) with an | |
| 189 | + | **Advisory** chip, and in the summary of reviews at the top of the pull | |
| 190 | + | request, but it counts toward nothing: | |
| 191 | + | ||
| 192 | + | - It never satisfies the approvals a branch requires, nor a | |
| 193 | + | [code owner's](/guides/codeowners/) approval. A person still approves. | |
| 194 | + | - A request for changes from an agent never blocks a merge and never sends | |
| 195 | + | g1t back to revise; a person's does. | |
| 196 | + | - It is never counted as a review on the profile of the person it acted | |
| 197 | + | for. | |
| 198 | + | ||
| 199 | + | What an agent may do on a pull request is capped by the person it acts | |
| 200 | + | for: they must be able to read the repository and comment on it, as if | |
| 201 | + | they wrote it themselves, and an archived repository refuses it. Beyond | |
| 202 | + | that: | |
| 203 | + | ||
| 204 | + | - An agent doesn't review a draft, nor a closed or merged pull request. It | |
| 205 | + | can still comment on them. | |
| 206 | + | - An agent writes at most 5 comments and reviews on one issue or pull | |
| 207 | + | request an hour. | |
| 208 | + | - Mentioning `@g1t` in an agent's comment doesn't put g1t to work, and an | |
| 209 | + | agent can't give commands on [dependency updates](/guides/dependency-updates/). | |
| 210 | + | ||
| 211 | + | The person it acted for can edit and delete what the agent wrote, as its | |
| 212 | + | author could, and so can anyone with the Maintain | |
| 213 | + | [role](/guides/access-and-roles/) or higher. A review stays, as a | |
| 214 | + | person's does. | |
| 215 | + | ||
| 216 | + | People mentioned in an agent's comment, and everyone subscribed to the | |
| 217 | + | pull request, hear of it in their [inbox](/guides/inbox/) as from | |
| 218 | + | **Margo (agent)**, with "(advisory)" on a review. It publishes | |
| 219 | + | `comment.created` as a person's comment does, with `agent` (its `id`, | |
| 220 | + | `handle`, `display_name` and `avatar_seed`), `acting_for`, and for a | |
| 221 | + | review `advisory: true` and the verdict, so [webhooks](/guides/webhooks/) | |
| 222 | + | see it too; its `actor` is the person it acted for. Through the API and | |
| 223 | + | the MCP server, each such comment has `agent`, `acting_for` and, for a | |
| 224 | + | review, `advisory: true`; its `author` is the agent, of kind `agent`. | |
| 225 | + | ||
| 179 | 226 | ## Drafts, closing and reopening | |
| 180 | 227 | ||
| 181 | 228 | A draft is still being worked on: it can be reviewed, but it cannot merge | |
| ⋯ | |||
| 238 | 285 | removed for everyone and cannot be brought back. | |
| 239 | 286 | ||
| 240 | 287 | A review that approved or requested changes can be edited but not deleted, | |
| 241 | − | so its verdict stays on record. The notes in the timeline of what happened, | |
| 288 | + | so its verdict stays on record. What an | |
| 289 | + | [agent wrote as itself](#agent-reviews) is the person it acted for's to | |
| 290 | + | edit and delete. The notes in the timeline of what happened, | |
| 242 | 291 | such as "closed this", cannot be edited or deleted. | |
| 243 | 292 | ||
| 244 | 293 | Through the API, `PATCH /repos/{owner}/{name}/issues/comments/{comment_id}` | |
| 716 | 716 | ||
| 717 | 717 | **The contribution calendar.** The last year as a square a day, a column a | |
| 718 | 718 | week, shaded more strongly the more you did that day, with the total | |
| 719 | − | above it ("128 contributions in the last year"). A contribution is an | |
| 720 | − | issue or pull request you opened (or g1t opened for you) and a review you | |
| 721 | − | gave. Days are counted in UTC. Hover over a square, or tap it, to see its | |
| 722 | − | day and count. On a narrow screen the calendar scrolls sideways inside | |
| 723 | − | its card, starting at today. | |
| 719 | + | above it ("128 contributions in the last year"). A contribution is a | |
| 720 | + | commit you pushed, an issue or pull request you opened (or g1t opened for | |
| 721 | + | you), and a review you gave. Days are counted in UTC. Hover over a square, | |
| 722 | + | or tap it, to see its day, its count and how many were commits ("5 | |
| 723 | + | contributions on Oct 4, 2026, 3 of them commits"). On a narrow screen the | |
| 724 | + | calendar scrolls sideways inside its card, starting at today. | |
| 725 | + | ||
| 726 | + | Commits count like this: | |
| 727 | + | ||
| 728 | + | - **Pushed to the default branch, or to `gh-pages`.** Commits on other | |
| 729 | + | branches count once they reach the default branch, which is usually a | |
| 730 | + | pull request, and the pull request is counted already. | |
| 731 | + | - **Credited to whoever pushed,** on the day of the push, not to the | |
| 732 | + | commits' authors. Pushes with an agent's, a workspace's or a workflow | |
| 733 | + | job's token are not counted on anyone's calendar. | |
| 734 | + | - **The new commits along the branch's own line,** at most 50 a push. A | |
| 735 | + | merge commit counts once. The first push of a branch counts one, so | |
| 736 | + | importing a long history doesn't fill a single day. | |
| 737 | + | - **Only from the time this was added:** pushes before 9 October 2026 are | |
| 738 | + | not counted. | |
| 724 | 739 | ||
| 725 | 740 | **Edit it** in [Settings → Profile](https://g1t.sh/settings/profile). Every | |
| 726 | 741 | field is optional. The bio takes up to 160 characters and is also what a |
| 118 | 118 | secrets and variables, an environment's `env`, a job's `outputs` and | |
| 119 | 119 | `matrix`, labels and headers come back exactly as they were written. | |
| 120 | 120 | ||
| 121 | + | Every person in a response, such as an issue's `author`, a workspace's | |
| 122 | + | members or whoever `whoami` is, has two usernames: | |
| 123 | + | ||
| 124 | + | - `username` is lowercased. It is what an account is found, linked and | |
| 125 | + | mentioned by, and it never changes case, so match on it. | |
| 126 | + | - `display_username` is the username as its owner wrote it, such as | |
| 127 | + | `Ana`. It is the same as `username` when they chose no case. Show it. | |
| 128 | + | ||
| 129 | + | ```json | |
| 130 | + | { "id": "usr_01kkntcg1eeb98j62xjm7eh09p", "username": "ana", "display_username": "Ana" } | |
| 131 | + | ``` | |
| 132 | + | ||
| 133 | + | Requests take a username in any case: `Ana` and `ana` are the same person. | |
| 134 | + | ||
| 121 | 135 | A successful request answers `200` with the result as the body: an object, | |
| 122 | 136 | a list, or `true` for a deletion. There is no envelope around it. | |
| 123 | 137 |
| 72 | 72 | ### Results | |
| 73 | 73 | ||
| 74 | 74 | A result is the operation's answer as JSON text, with `snake_case` fields, | |
| 75 | − | as the REST API returns it: | |
| 75 | + | as the REST API returns it. Each person in it has a lowercased `username` | |
| 76 | + | and a `display_username`, the case they chose | |
| 77 | + | ([Usernames](/reference/api/#requests-and-responses)): | |
| 76 | 78 | ||
| 77 | 79 | ```json | |
| 78 | 80 | { | |
| ⋯ | |||
| 347 | 349 | | Action | What it does | Required | Scope | | |
| 348 | 350 | | --- | --- | --- | --- | | |
| 349 | 351 | | [`list`](/reference/api/pull-requests/list-pull-requests/) | Pull requests, newest first. `open` covers drafts and those ready for review. `label`, `milestone` and `base` filter them. | `repo` | `pull_requests:read` | | |
| 350 | − | | [`get`](/reference/api/pull-requests/get-pull-request/) | Status, head commit, comments and reviews, who is asked to review (`pull.reviewers`, and `pull.team_reviewers` as `workspace/team`), its issue, its checks (`statuses`, and `required_checks`: each check the default branch requires, as `success`, `failure`, `pending` or `expected`), `code_owners` (whose approval the changed files need, and what is still `missing`), `behind`, and `overlaps`. | `repo`, `number` | `pull_requests:read` | | |
| 352 | + | | [`get`](/reference/api/pull-requests/get-pull-request/) | Status, head commit, comments and reviews, who is asked to review (`pull.reviewers`, and `pull.team_reviewers` as `workspace/team`), its issue, its checks (`statuses`, and `required_checks`: each check the default branch requires, as `success`, `failure`, `pending` or `expected`), `code_owners` (whose approval the changed files need, and what is still `missing`), `behind`, and `overlaps`. A comment or review one of the workspace's agents wrote as itself has `agent` and `acting_for`; an agent's review has `advisory: true` and never counts toward approvals ([agent reviews](/guides/pull-requests/#agent-reviews)). | `repo`, `number` | `pull_requests:read` | | |
| 351 | 353 | | [`changes`](/reference/api/pull-requests/get-pull-request-changes/) | The files it changes, with line-by-line diffs. | `repo`, `number` | `pull_requests:read` | | |
| 352 | 354 | | [`create`](/reference/api/pull-requests/create-pull-request/) | Open a draft pull request with its own fork and get its git remote; or, with `branch`, one from a branch already pushed. Give `issue` whenever there is one. It merges into the default branch unless `base` names another. | `repo` | `pull_requests:write` | | |
| 353 | 355 | | [`update`](/reference/api/pull-requests/update-pull-request/) | Change its `base` (the branch it merges into; Write role), `labels`, `milestone`, `assignees` or `reviewers`. `state` `open` reopens it and `closed` closes it. | `repo`, `number` | `pull_requests:write` | | |
Binary or large file; its contents are not shown.
| 669 | 669 | font-size: max(16px, 1em); | |
| 670 | 670 | } | |
| 671 | 671 | } | |
| 672 | + | ||
| 673 | + | /* A card's Markdown preview in chat (components/chat/card.tsx): the same Markdown, smaller and tighter. */ | |
| 674 | + | .card-md .prose { | |
| 675 | + | @apply text-[0.8125rem] leading-relaxed; | |
| 676 | + | } | |
| 677 | + | .card-md .prose > * + * { | |
| 678 | + | @apply mt-2; | |
| 679 | + | } | |
| 680 | + | .card-md .prose p, | |
| 681 | + | .card-md .prose li { | |
| 682 | + | @apply text-[0.8125rem] text-fg/85; | |
| 683 | + | } | |
| 684 | + | .card-md .prose h1, | |
| 685 | + | .card-md .prose h2, | |
| 686 | + | .card-md .prose h3, | |
| 687 | + | .card-md .prose h4 { | |
| 688 | + | @apply mt-3 border-0 pb-0 text-sm font-semibold; | |
| 689 | + | } | |
| 690 | + | .card-md .prose ul, | |
| 691 | + | .card-md .prose ol { | |
| 692 | + | @apply space-y-0.5 pl-5; | |
| 693 | + | } | |
| 694 | + | .card-md .prose pre { | |
| 695 | + | @apply p-2.5 text-xs leading-5; | |
| 696 | + | } |
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
This change is too large to show in full.