Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 1 | //! People: a workspace's directory of the people in it, and what each |
| 2 | //! person's place is: a title and what they own. Kept by identity beside | |
| 3 | //! membership. | |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 4 | //! |
| 5 | //! **Profiles.** A member's title and what they own are theirs to write, | |
| Merge branch 'rm-orgchart-getstarted' | 6 | //! and the workspace's owners can write anyone's. |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 7 | //! |
| 8 | //! Every method is served by identity at `POST /rpc/<method>`. | |
| 9 | ||
| 10 | use serde::{Deserialize, Serialize}; | |
| 11 | ||
| 12 | use crate::teams::{TeamChannel, TeamLead, TeamRef, TeamRole, TeamVisibility}; | |
| 13 | use crate::{Role, User}; | |
| 14 | ||
| 15 | /// The longest title. | |
| 16 | pub const MAX_TITLE_LENGTH: usize = 80; | |
| 17 | /// The most things one person owns, as their profile lists them. | |
| 18 | pub const MAX_OWNS: usize = 8; | |
| 19 | /// The longest of them. | |
| 20 | pub const MAX_OWNS_LENGTH: usize = 60; | |
| 21 | ||
| Merge branch 'rm-orgchart-getstarted' | 22 | /// One member, as the directory and their profile show them. |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 23 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 24 | pub struct DirectoryPerson { | |
| 25 | pub user_id: String, | |
| 26 | pub username: String, | |
| 27 | /// The username as its owner wrote it, when that differs. | |
| 28 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 29 | pub display_username: Option<String>, | |
| 30 | pub name: Option<String>, | |
| 31 | pub avatar: Option<String>, | |
| 32 | /// From their public profile. | |
| 33 | pub bio: Option<String>, | |
| 34 | pub location: Option<String>, | |
| 35 | pub pronouns: Option<String>, | |
| 36 | /// An IANA name, such as `America/Denver`, for their local time. | |
| 37 | pub timezone: Option<String>, | |
| 38 | /// Owner or member. | |
| 39 | pub role: Role, | |
| 40 | /// Their title in this workspace. | |
| 41 | pub title: Option<String>, | |
| 42 | /// What they own: a few short phrases. | |
| 43 | pub owns: Vec<String>, | |
| 44 | /// When they joined the workspace (RFC 3339). | |
| 45 | pub joined_at: String, | |
| 46 | } | |
| 47 | ||
| 48 | /// One person on a team, as the directory names them. | |
| 49 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 50 | pub struct TeamPersonRef { | |
| 51 | pub username: String, | |
| 52 | pub role: TeamRole, | |
| 53 | } | |
| 54 | ||
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 55 | /// A team as the directory needs it: who is on it, by username, and who |
| 56 | /// leads it. | |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 57 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 58 | pub struct DirectoryTeam { | |
| 59 | pub slug: String, | |
| 60 | pub name: String, | |
| 61 | pub description: Option<String>, | |
| 62 | pub visibility: TeamVisibility, | |
| 63 | pub parent: Option<TeamRef>, | |
| 64 | pub lead: Option<TeamLead>, | |
| 65 | pub channel: Option<TeamChannel>, | |
| 66 | /// Its own people, not its child teams'. | |
| 67 | pub people: Vec<TeamPersonRef>, | |
| 68 | /// Repositories it has a role on itself. | |
| 69 | pub repos_count: u32, | |
| 70 | } | |
| 71 | ||
| 72 | /// `people_directory`: everyone in a workspace and its teams, as the | |
| 73 | /// viewer may see them (secret teams only for their people and owners). | |
| 74 | /// Members only. Returns `Outcome<PeopleDirectory>`. | |
| 75 | #[derive(Debug, Serialize, Deserialize)] | |
| 76 | pub struct PeopleArgs { | |
| 77 | pub viewer: crate::Viewer, | |
| 78 | pub workspace: String, | |
| 79 | } | |
| 80 | ||
| 81 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 82 | pub struct PeopleDirectory { | |
| 83 | pub people: Vec<DirectoryPerson>, | |
| 84 | pub teams: Vec<DirectoryTeam>, | |
| 85 | /// What members get on every repository: `none`, `read`, `write` or `admin`. | |
| 86 | pub base_permission: String, | |
| 87 | /// Whether the viewer owns the workspace: they can set anyone's | |
| Merge branch 'rm-orgchart-getstarted' | 88 | /// title and what they own. |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 89 | pub can_manage: bool, |
| 90 | } | |
| 91 | ||
| Merge branch 'rm-orgchart-getstarted' | 92 | /// `set_member_profile`: what is given changes; the rest stays. Returns |
| 93 | /// `Outcome<DirectoryPerson>`. | |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 94 | #[derive(Debug, Default, Serialize, Deserialize)] |
| 95 | pub struct SetMemberProfileArgs { | |
| 96 | pub actor: User, | |
| 97 | pub workspace: String, | |
| 98 | pub username: String, | |
| 99 | #[serde(default)] | |
| 100 | pub title: Option<String>, | |
| 101 | #[serde(default)] | |
| 102 | pub owns: Option<Vec<String>>, | |
| 103 | #[serde(default)] | |
| 104 | pub surface: Option<crate::audit::Surface>, | |
| 105 | } | |
| 106 | ||
| 107 | /// A title as stored: trimmed, at most [`MAX_TITLE_LENGTH`] characters, | |
| 108 | /// `None` when nothing is left. | |
| 109 | pub fn clean_title(title: &str) -> Option<String> { | |
| 110 | let title: String = title.split_whitespace().collect::<Vec<_>>().join(" ").chars().take(MAX_TITLE_LENGTH).collect(); | |
| 111 | let title = title.trim().to_owned(); | |
| 112 | (!title.is_empty()).then_some(title) | |
| 113 | } | |
| 114 | ||
| 115 | /// What someone owns, as stored: each phrase trimmed and cut to | |
| 116 | /// [`MAX_OWNS_LENGTH`], blanks and repeats (ignoring case) left out, at | |
| 117 | /// most [`MAX_OWNS`]. | |
| 118 | pub fn clean_owns(owns: &[String]) -> Vec<String> { | |
| 119 | let mut out: Vec<String> = Vec::new(); | |
| 120 | for phrase in owns { | |
| 121 | let phrase: String = phrase.split_whitespace().collect::<Vec<_>>().join(" ").chars().take(MAX_OWNS_LENGTH).collect(); | |
| 122 | let phrase = phrase.trim().trim_end_matches(['.', ',', ';']).trim().to_owned(); | |
| 123 | if phrase.is_empty() || out.iter().any(|kept| kept.eq_ignore_ascii_case(&phrase)) { | |
| 124 | continue; | |
| 125 | } | |
| 126 | out.push(phrase); | |
| 127 | if out.len() == MAX_OWNS { | |
| 128 | break; | |
| 129 | } | |
| 130 | } | |
| 131 | out | |
| 132 | } | |
| 133 | ||
| Merge branch 'rm-orgchart-getstarted' | 134 | /// What may change on someone's profile: their title and what they own, |
| 135 | /// by themselves or an owner. | |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 136 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 137 | pub enum ProfileField { | |
| 138 | Title, | |
| 139 | Owns, | |
| 140 | } | |
| 141 | ||
| 142 | /// Whether `actor` may change `field` on someone's profile. | |
| 143 | pub fn may_edit_profile(field: ProfileField, is_self: bool, owner: bool) -> bool { | |
| 144 | match field { | |
| 145 | ProfileField::Title | ProfileField::Owns => is_self || owner, | |
| 146 | } | |
| 147 | } | |
| 148 | ||
| 149 | #[cfg(test)] | |
| 150 | mod tests { | |
| 151 | use super::*; | |
| 152 | ||
| 153 | #[test] | |
| 154 | fn titles_are_tidied() { | |
| 155 | assert_eq!(clean_title(" Staff Engineer "), Some("Staff Engineer".to_owned())); | |
| 156 | assert_eq!(clean_title(" "), None); | |
| 157 | assert_eq!(clean_title(&"a".repeat(200)).map(|t| t.len()), Some(MAX_TITLE_LENGTH)); | |
| 158 | } | |
| 159 | ||
| 160 | #[test] | |
| 161 | fn what_someone_owns_is_kept_short_and_once() { | |
| 162 | let owns = clean_owns(&[ | |
| 163 | " storefront ".into(), | |
| 164 | "Storefront".into(), | |
| 165 | "".into(), | |
| 166 | "the release process.".into(), | |
| 167 | "x".repeat(100), | |
| 168 | ]); | |
| 169 | assert_eq!(owns[0], "storefront"); | |
| 170 | assert_eq!(owns[1], "the release process"); | |
| 171 | assert_eq!(owns[2].len(), MAX_OWNS_LENGTH); | |
| 172 | assert_eq!(owns.len(), 3); | |
| 173 | let many: Vec<String> = (0..20).map(|i| format!("thing {i}")).collect(); | |
| 174 | assert_eq!(clean_owns(&many).len(), MAX_OWNS); | |
| 175 | } | |
| 176 | ||
| 177 | #[test] | |
| Merge branch 'rm-orgchart-getstarted' | 178 | fn people_write_their_own_and_owners_anyones() { |
| People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how. | 179 | assert!(may_edit_profile(ProfileField::Title, true, false)); |
| 180 | assert!(may_edit_profile(ProfileField::Owns, false, true)); | |
| 181 | assert!(!may_edit_profile(ProfileField::Owns, false, false)); | |
| 182 | } | |
| 183 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.