Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
744 linesCodeBlameRaw

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.

Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1//! Teams: groups of a workspace's members, given roles on repositories
2//! together, mentioned together and asked to review together.
3//!
4//! **Who is in one.** A team has **maintainers**, who manage its people
5//! and settings, and **members**. Only members of the workspace can be in
6//! its teams; leaving the workspace takes a person out of all of them.
7//! Owners of the workspace manage every team, whether or not they are in
8//! it.
9//!
10//! **Visibility.** A **visible** team is seen by every member of the
11//! workspace. A **secret** team is seen only by its own people and the
12//! workspace's owners. Secret teams cannot be nested.
13//!
14//! **Nesting.** A team can have a parent. A child team inherits its
15//! parent's roles on repositories (and its parent's parent's), and a
16//! mention or review request for the parent reaches the child teams'
17//! people too. A team's own people never get anything from its children.
18//!
19//! **Repository access.** A team is given a [`RepoRole`] on a repository
20//! as a person is: a row in `repo_grants` whose principal is the team.
21//! Identity resolves it into the same [`RepoGrant`](crate::access::RepoGrant)s
22//! on every person in the team and in its child teams, so `access::can`
23//! decides with it as with any other grant: the highest role wins.
24//!
25//! **Review requests.** A pull request can ask a team to review it. With
26//! [`ReviewAssignment`] off, everyone in the team is asked. With it on,
27//! g1t picks `count` people from it (never the pull request's author) and
28//! asks them; the team stays shown as requested beside them.
29//!
30//! Every method is served by identity at `POST /rpc/<method>`. Changing a
31//! team is for people, signed in or with a personal access token; never
32//! an agent's or a workspace's token.
33
34use serde::{Deserialize, Serialize};
35
Merge branch 'worktree-agent-ad7c6d88d93adc817'36use crate::{Role, User};
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar37use crate::access::RepoRole;
38use crate::repos::RepoPath;
39
40/// The most teams one workspace can have.
41pub const MAX_TEAMS: u32 = 500;
42/// The longest team name.
43pub const MAX_NAME_LENGTH: usize = 80;
44/// The longest description.
45pub const MAX_DESCRIPTION_LENGTH: usize = 280;
46/// How deep teams can nest: a team, its child, and so on.
47pub const MAX_DEPTH: usize = 8;
48/// The most people review assignment picks for one request.
49pub const MAX_ASSIGNED: u32 = 10;
50
51/// Who can see a team.
52#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case")]
54pub enum TeamVisibility {
55 /// Every member of the workspace.
56 #[default]
57 Visible,
58 /// The team's own people and the workspace's owners.
59 Secret,
60}
61
62impl TeamVisibility {
63 pub fn as_str(self) -> &'static str {
64 match self {
65 TeamVisibility::Visible => "visible",
66 TeamVisibility::Secret => "secret",
67 }
68 }
69
70 pub fn parse(text: &str) -> Option<TeamVisibility> {
71 match text.trim().to_ascii_lowercase().as_str() {
72 "visible" | "closed" => Some(TeamVisibility::Visible),
73 "secret" => Some(TeamVisibility::Secret),
74 _ => None,
75 }
76 }
77}
78
Merge branch 'worktree-agent-ad7c6d88d93adc817'79/// Who may create a workspace's teams: a workspace setting, changed by
80/// its owners (`set_team_creation`). Stored in `workspaces.team_creation`,
81/// NULL for the default.
82#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum TeamCreation {
85 /// Any member with a confirmed email address.
86 #[default]
87 Members,
88 /// The workspace's owners only.
89 Owners,
90}
91
92impl TeamCreation {
93 pub const ALL: [TeamCreation; 2] = [TeamCreation::Members, TeamCreation::Owners];
94
95 pub fn as_str(self) -> &'static str {
96 match self {
97 TeamCreation::Members => "members",
98 TeamCreation::Owners => "owners",
99 }
100 }
101
102 pub fn parse(text: &str) -> Option<TeamCreation> {
103 match text.trim().to_ascii_lowercase().as_str() {
104 "members" | "member" | "any" => Some(TeamCreation::Members),
105 "owners" | "owner" => Some(TeamCreation::Owners),
106 _ => None,
107 }
108 }
109
110 /// Whether someone with `role` in the workspace may create a team.
111 pub fn allows(self, role: Role) -> bool {
112 match self {
113 TeamCreation::Members => true,
114 TeamCreation::Owners => role == Role::Owner,
115 }
116 }
117}
118
119/// `set_team_creation`: who may create the workspace's teams. Owners
120/// only, as a person. Returns `Outcome<TeamCreation>`.
121#[derive(Debug, Serialize, Deserialize)]
122pub struct SetTeamCreationArgs {
123 pub actor: User,
124 pub slug: String,
125 pub team_creation: TeamCreation,
126 #[serde(default)]
127 pub surface: Option<crate::audit::Surface>,
128}
129
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar130/// A person's place in a team.
131#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
132#[serde(rename_all = "snake_case")]
133pub enum TeamRole {
134 Member,
135 /// Manages the team's people and settings.
136 Maintainer,
137}
138
139impl TeamRole {
140 pub fn as_str(self) -> &'static str {
141 match self {
142 TeamRole::Member => "member",
143 TeamRole::Maintainer => "maintainer",
144 }
145 }
146
147 pub fn parse(text: &str) -> Option<TeamRole> {
148 match text.trim().to_ascii_lowercase().as_str() {
149 "member" => Some(TeamRole::Member),
150 "maintainer" => Some(TeamRole::Maintainer),
151 _ => None,
152 }
153 }
154}
155
156/// How review assignment picks people.
157#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(rename_all = "snake_case")]
159pub enum ReviewAlgorithm {
160 /// Whoever was asked least recently by this team goes first.
161 #[default]
162 RoundRobin,
163 /// Whoever has the fewest pull requests waiting on their review goes
164 /// first.
165 LoadBalance,
166}
167
168impl ReviewAlgorithm {
169 pub fn as_str(self) -> &'static str {
170 match self {
171 ReviewAlgorithm::RoundRobin => "round_robin",
172 ReviewAlgorithm::LoadBalance => "load_balance",
173 }
174 }
175
176 pub fn parse(text: &str) -> Option<ReviewAlgorithm> {
177 match text.trim().to_ascii_lowercase().as_str() {
178 "round_robin" => Some(ReviewAlgorithm::RoundRobin),
179 "load_balance" => Some(ReviewAlgorithm::LoadBalance),
180 _ => None,
181 }
182 }
183}
184
185/// What happens when a team is asked to review a pull request.
186#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
187pub struct ReviewAssignment {
188 /// Off: everyone in the team is asked. On: `count` people are picked.
189 pub enabled: bool,
190 pub algorithm: ReviewAlgorithm,
191 /// How many people to pick, 1 to [`MAX_ASSIGNED`]. People from the team
192 /// already asked count towards it.
193 pub count: u32,
194 /// Leave out anyone with `busy_at` or more open pull requests waiting
195 /// on their review.
196 pub skip_busy: bool,
197 pub busy_at: u32,
198 /// Also pick from the people of its child teams.
199 pub include_child_teams: bool,
200 /// Usernames never picked.
201 #[serde(default)]
202 pub excluded: Vec<String>,
203 /// Also tell the rest of the team when people are picked.
204 pub notify_team: bool,
205}
206
207impl Default for ReviewAssignment {
208 fn default() -> Self {
209 ReviewAssignment {
210 enabled: false,
211 algorithm: ReviewAlgorithm::RoundRobin,
212 count: 1,
213 skip_busy: false,
214 busy_at: 5,
215 include_child_teams: false,
216 excluded: Vec::new(),
217 notify_team: false,
218 }
219 }
220}
221
222impl ReviewAssignment {
223 /// The same, with every number within bounds and the usernames
224 /// lowercased, each once.
225 pub fn bounded(mut self) -> Self {
226 self.count = self.count.clamp(1, MAX_ASSIGNED);
227 self.busy_at = self.busy_at.clamp(1, 100);
228 let mut excluded: Vec<String> = Vec::new();
229 for name in self.excluded {
230 let name = name.trim().trim_start_matches('@').to_lowercase();
231 if !name.is_empty() && !excluded.contains(&name) {
232 excluded.push(name);
233 }
234 }
235 excluded.truncate(100);
236 self.excluded = excluded;
237 self
238 }
239}
240
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.241/// Who leads a team: a person on 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.242#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
243#[serde(tag = "kind", rename_all = "snake_case")]
244pub enum TeamLead {
245 User {
246 username: String,
247 #[serde(default)]
248 name: Option<String>,
249 #[serde(default)]
250 avatar: Option<String>,
251 },
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.252 /// An agent of g1t's retired hosted agents, by its id. Kept only so a
253 /// lead written before they were removed still parses; identity never
254 /// answers with one, and no team can be given one.
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.255 Agent { agent_id: String },
256}
257
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.258/// A lead as written to `update_team`: `@username` (or a bare username).
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.259#[derive(Clone, Debug, PartialEq, Eq)]
260pub enum LeadInput {
261 User(String),
262}
263
264/// `lead` as `update_team` takes it; `None` when it is not one. An empty
265/// string means no lead, and is not parsed here.
266pub fn parse_lead(text: &str) -> Option<LeadInput> {
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.267 let name = text.trim().trim_start_matches('@').to_lowercase();
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.268 crate::is_valid_namespace(&name).then_some(LeadInput::User(name))
269}
270
271/// A team's chat channel.
272#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
273pub struct TeamChannel {
274 /// Its id in the chat service.
275 pub id: String,
276 /// Its name, without `#`, as it was when chosen.
277 pub name: String,
278}
279
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar280/// A team as another names it: its parent, or a child.
281#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
282pub struct TeamRef {
283 pub slug: String,
284 pub name: String,
285}
286
287#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
288pub struct Team {
289 pub id: String,
290 /// Its workspace's slug.
291 pub workspace: String,
292 /// Its name in URLs and mentions: `@<workspace>/<slug>`.
293 pub slug: String,
294 pub name: String,
295 pub description: Option<String>,
296 pub visibility: TeamVisibility,
297 pub parent: Option<TeamRef>,
298 /// Whether its people are notified when it is mentioned.
299 pub notify: bool,
300 pub review_assignment: ReviewAssignment,
301 /// Its own people, not counting child teams'.
302 pub members_count: u32,
303 /// Repositories it has a role on itself, not counting inherited ones.
304 pub repos_count: u32,
305 pub child_teams_count: u32,
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.306 /// Who leads it, if anyone.
307 #[serde(default)]
308 pub lead: Option<TeamLead>,
309 /// Its chat channel, if it has one.
310 #[serde(default)]
311 pub channel: Option<TeamChannel>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar312 /// The viewer's place in it, if any.
313 pub viewer_role: Option<TeamRole>,
314 /// Whether the viewer may change it: an owner of the workspace, or one
315 /// of its maintainers.
316 pub can_manage: bool,
317 /// RFC 3339.
318 pub created_at: String,
319 pub updated_at: String,
320}
321
322impl Team {
323 /// How it is mentioned: `@acme/backend`.
324 pub fn handle(&self) -> String {
325 format!("@{}/{}", self.workspace, self.slug)
326 }
327}
328
329/// One person in a team.
330#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
331pub struct TeamMember {
332 pub username: String,
333 pub name: Option<String>,
334 pub avatar: Option<String>,
335 pub role: TeamRole,
336 /// The child team they are in, when listed through one; null for the
337 /// team's own people.
338 pub via: Option<String>,
339}
340
341/// A repository a team has a role on.
342#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
343pub struct TeamRepo {
344 /// `workspace/name`.
345 pub repo: String,
346 pub repo_id: String,
347 pub role: RepoRole,
348 /// The parent team it comes from, by slug, when the team inherits it.
349 pub inherited_from: Option<String>,
350}
351
352/// A team with a role on a repository, as its Access settings list it.
353#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
354pub struct RepoTeam {
355 pub slug: String,
356 pub name: String,
357 pub role: RepoRole,
358 pub members_count: u32,
359 pub visibility: TeamVisibility,
360}
361
362/// A team as services need it to notify or ask its people: everyone in it,
363/// and its settings. Never shown as is.
364#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
365pub struct ResolvedTeam {
366 pub id: String,
367 pub workspace: String,
368 pub slug: String,
369 pub name: String,
370 pub visibility: TeamVisibility,
371 pub notify: bool,
372 pub review_assignment: ReviewAssignment,
373 /// Its own people.
374 pub members: Vec<TeamPerson>,
375 /// The people of its child teams (and theirs) who are not its own.
376 pub child_members: Vec<TeamPerson>,
377 /// Its role on the repository asked about, its own or inherited.
378 pub repo_role: Option<RepoRole>,
379 /// Whether the person asked about (`asker`) may see it, and so mention
380 /// it or ask it to review: a member of its workspace, and for a secret
381 /// team, in it or an owner.
382 #[serde(default)]
383 pub asker_sees: bool,
384}
385
386impl ResolvedTeam {
387 /// Everyone a mention or a request reaches: its own people, then its
388 /// child teams'.
389 pub fn everyone(&self) -> impl Iterator<Item = &TeamPerson> {
390 self.members.iter().chain(self.child_members.iter())
391 }
392
393 pub fn handle(&self) -> String {
394 format!("@{}/{}", self.workspace, self.slug)
395 }
396}
397
398#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
399pub struct TeamPerson {
400 pub id: String,
401 pub username: String,
402}
403
404/// A team's slug from its name: lowercase letters, digits and single
405/// hyphens, as mentions spell it. `None` when nothing is left.
406pub fn slug_of(name: &str) -> Option<String> {
407 let mut slug = String::new();
408 for c in name.trim().chars() {
409 if c.is_ascii_alphanumeric() {
410 slug.push(c.to_ascii_lowercase());
411 } else if !slug.is_empty() && !slug.ends_with('-') {
412 slug.push('-');
413 }
414 }
415 let slug = slug.trim_end_matches('-').chars().take(60).collect::<String>();
416 let slug = slug.trim_end_matches('-').to_owned();
417 is_valid_slug(&slug).then_some(slug)
418}
419
420/// Whether `slug` is a team slug: 1 to 60 lowercase letters, digits and
421/// single hyphens, not starting or ending with one.
422pub fn is_valid_slug(slug: &str) -> bool {
423 !slug.is_empty()
424 && slug.len() <= 60
425 && slug.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
426 && !slug.starts_with('-')
427 && !slug.ends_with('-')
428 && !slug.contains("--")
429}
430
431/// `@workspace/team` as written, split; `None` if it is not that shape.
432pub fn parse_handle(text: &str) -> Option<(String, String)> {
433 let text = text.trim().strip_prefix('@')?;
434 let (workspace, slug) = text.split_once('/')?;
435 let workspace = workspace.to_lowercase();
436 let slug = slug.to_lowercase();
437 (crate::is_valid_namespace(&workspace) && is_valid_slug(&slug)).then_some((workspace, slug))
438}
439
440// --- Identity methods ---------------------------------------------------------
441
442/// `list_teams`: the teams of a workspace the viewer can see, theirs first,
443/// then by name. Members only. `query` narrows by name or slug. Returns
444/// `Outcome<Vec<Team>>`.
445#[derive(Debug, Serialize, Deserialize)]
446pub struct ListTeamsArgs {
447 pub viewer: crate::Viewer,
448 pub workspace: String,
449 #[serde(default)]
450 pub query: Option<String>,
451}
452
453/// `get_team`, `child_teams`, `team_repos`: one team, its child teams, or
454/// the repositories it has a role on (its own and inherited), as the
455/// viewer may see them. `team_members` takes `include_child_teams`.
456#[derive(Debug, Serialize, Deserialize)]
457pub struct TeamArgs {
458 pub viewer: crate::Viewer,
459 pub workspace: String,
460 pub team: String,
461 /// `team_members` only: also list the people of child teams.
462 #[serde(default)]
463 pub include_child_teams: bool,
464}
465
Merge branch 'worktree-agent-ad7c6d88d93adc817'466/// `create_team`. Members may create a team, unless the workspace's
467/// [`TeamCreation`] says owners only, and become its maintainer; a team
468/// with a parent needs an owner, or a maintainer of the parent. Returns
469/// `Outcome<Team>`.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar470#[derive(Debug, Serialize, Deserialize)]
471pub struct CreateTeamArgs {
472 pub actor: User,
473 pub workspace: String,
474 pub name: String,
475 /// Defaults to one made from the name.
476 #[serde(default)]
477 pub slug: Option<String>,
478 #[serde(default)]
479 pub description: Option<String>,
480 #[serde(default)]
481 pub visibility: Option<TeamVisibility>,
482 /// The parent's slug.
483 #[serde(default)]
484 pub parent: Option<String>,
485 #[serde(default)]
486 pub notify: Option<bool>,
487 /// People to add as members, by username, besides the creator.
488 #[serde(default)]
489 pub members: Vec<String>,
490 #[serde(default)]
491 pub surface: Option<crate::audit::Surface>,
492}
493
494/// `update_team`: what is given changes; the rest stays. `parent` set to an
495/// empty string takes the team out from under its parent. Owners and the
496/// team's maintainers. Returns `Outcome<Team>`.
497#[derive(Debug, Default, Serialize, Deserialize)]
498pub struct UpdateTeamArgs {
499 pub actor: User,
500 pub workspace: String,
501 pub team: String,
502 #[serde(default)]
503 pub name: Option<String>,
504 #[serde(default)]
505 pub slug: Option<String>,
506 #[serde(default)]
507 pub description: Option<String>,
508 #[serde(default)]
509 pub visibility: Option<TeamVisibility>,
510 #[serde(default)]
511 pub parent: Option<String>,
512 #[serde(default)]
513 pub notify: Option<bool>,
514 #[serde(default)]
515 pub review_assignment: Option<ReviewAssignment>,
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.516 /// Who leads it: `@username`, someone on the team; an empty string for
517 /// no lead.
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.518 #[serde(default)]
519 pub lead: Option<String>,
520 /// Its chat channel's id in the chat service; an empty string for none.
521 #[serde(default)]
522 pub channel_id: Option<String>,
523 /// That channel's name, without `#`; given with `channel_id`.
524 #[serde(default)]
525 pub channel_name: Option<String>,
526 #[serde(default)]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar527 pub surface: Option<crate::audit::Surface>,
528}
529
530/// `delete_team`: its child teams move up to its parent, and the roles it
531/// gave go with it. Owners and the team's maintainers. Returns
532/// `Outcome<bool>`.
533#[derive(Debug, Serialize, Deserialize)]
534pub struct DeleteTeamArgs {
535 pub actor: User,
536 pub workspace: String,
537 pub team: String,
538 #[serde(default)]
539 pub surface: Option<crate::audit::Surface>,
540}
541
542/// `set_team_member`: adds a member of the workspace to a team, or changes
543/// their role in it. Owners and the team's maintainers. Returns
544/// `Outcome<TeamMember>`.
545#[derive(Debug, Serialize, Deserialize)]
546pub struct SetTeamMemberArgs {
547 pub actor: User,
548 pub workspace: String,
549 pub team: String,
550 pub username: String,
551 pub role: TeamRole,
552 #[serde(default)]
553 pub surface: Option<crate::audit::Surface>,
554}
555
556/// `remove_team_member`: owners and the team's maintainers; anyone may
557/// leave a team themselves. Returns `Outcome<bool>`.
558#[derive(Debug, Serialize, Deserialize)]
559pub struct RemoveTeamMemberArgs {
560 pub actor: User,
561 pub workspace: String,
562 pub team: String,
563 pub username: String,
564 #[serde(default)]
565 pub surface: Option<crate::audit::Surface>,
566}
567
568/// `set_team_repo`: gives a team a role on a repository of its workspace,
569/// or changes it. Needs Admin on the repository. Returns
570/// `Outcome<TeamRepo>`.
571#[derive(Debug, Serialize, Deserialize)]
572pub struct SetTeamRepoArgs {
573 pub actor: User,
574 pub workspace: String,
575 pub team: String,
576 pub repo: RepoPath,
577 pub role: RepoRole,
578 #[serde(default)]
579 pub surface: Option<crate::audit::Surface>,
580}
581
582/// `remove_team_repo`: takes a team's role on a repository away. Admin on
583/// the repository, an owner, or one of the team's maintainers. Returns
584/// `Outcome<bool>`.
585#[derive(Debug, Serialize, Deserialize)]
586pub struct RemoveTeamRepoArgs {
587 pub actor: User,
588 pub workspace: String,
589 pub team: String,
590 pub repo: RepoPath,
591 #[serde(default)]
592 pub surface: Option<crate::audit::Surface>,
593}
594
595/// `user_teams`: the teams `username` is in within a workspace, as the
596/// viewer may see them. Members only. Returns `Outcome<Vec<Team>>`.
597#[derive(Debug, Serialize, Deserialize)]
598pub struct UserTeamsArgs {
599 pub viewer: crate::Viewer,
600 pub workspace: String,
601 pub username: String,
602}
603
604/// `team_memberships`: for each member of a workspace, the teams they are
605/// in that the viewer can see, for the Members page. Members only.
606/// Returns `Outcome<Vec<MemberTeams>>`.
607#[derive(Debug, Serialize, Deserialize)]
608pub struct TeamMembershipsArgs {
609 pub viewer: crate::Viewer,
610 pub workspace: String,
611}
612
613/// One person's teams in a workspace.
614#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
615pub struct MemberTeams {
616 pub username: String,
617 pub teams: Vec<TeamRef>,
618}
619
620/// `resolve_teams`: for services. Each team named `workspace/slug` (or
621/// `@workspace/slug`) that exists, with everyone in it and, given
622/// `repo_id`, its role on that repository. Missing teams are left out.
623/// Returns `Vec<ResolvedTeam>`.
624#[derive(Debug, Default, Serialize, Deserialize)]
625pub struct ResolveTeamsArgs {
626 pub teams: Vec<String>,
627 #[serde(default)]
628 pub repo_id: Option<String>,
629 /// A user id, for `asker_sees`.
630 #[serde(default)]
631 pub asker: Option<String>,
632}
633
634/// One owner a CODEOWNERS file names, as identity resolved it.
635#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
636pub struct ResolvedOwner {
637 pub owner: crate::codeowners::Owner,
638 pub check: crate::codeowners::OwnerCheck,
639 /// Who may answer for it, by username: the person, the account a
640 /// confirmed address belongs to, or everyone in a team and its child
641 /// teams. Empty when it did not resolve.
642 pub members: Vec<String>,
643 /// For a team: the team, as `workspace/slug`, after `@org/team` was
644 /// mapped to the repository's workspace.
645 #[serde(default)]
646 pub team: Option<String>,
647}
648
649/// `resolve_owners`: for services. Resolves the owners a CODEOWNERS file
650/// names on a repository: accounts, teams of the repository's workspace
651/// (`@org/team` with an `org` that is not a g1t workspace means the
652/// repository's own workspace), and confirmed email addresses, checking
653/// each has the Write role or higher on it. `g1t` resolves to itself.
654/// Returns `Vec<ResolvedOwner>`, in the order asked.
655#[derive(Debug, Serialize, Deserialize)]
656pub struct ResolveOwnersArgs {
657 pub repo_id: String,
658 /// The repository's workspace, by slug.
659 pub workspace: String,
660 pub owners: Vec<crate::codeowners::Owner>,
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666
667 #[test]
Merge branch 'worktree-agent-ad7c6d88d93adc817'668 fn who_may_create_teams() {
669 assert_eq!(TeamCreation::default(), TeamCreation::Members);
670 assert!(TeamCreation::Members.allows(Role::Member) && TeamCreation::Members.allows(Role::Owner));
671 assert!(!TeamCreation::Owners.allows(Role::Member) && TeamCreation::Owners.allows(Role::Owner));
672 for setting in TeamCreation::ALL {
673 assert_eq!(TeamCreation::parse(setting.as_str()), Some(setting));
674 assert_eq!(serde_json::to_value(setting).unwrap(), setting.as_str());
675 }
676 assert_eq!(TeamCreation::parse(" Owners "), Some(TeamCreation::Owners));
677 assert_eq!(TeamCreation::parse("maintainers"), None);
678 }
679
680 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar681 fn slugs_come_from_names() {
682 assert_eq!(slug_of("Backend").as_deref(), Some("backend"));
683 assert_eq!(slug_of(" Web & Mobile ").as_deref(), Some("web-mobile"));
684 assert_eq!(slug_of("SRE / On-call").as_deref(), Some("sre-on-call"));
685 assert_eq!(slug_of("!!!"), None);
686 assert_eq!(slug_of(&"a".repeat(80)).map(|slug| slug.len()), Some(60));
687 assert!(is_valid_slug("platform-2"));
688 assert!(!is_valid_slug("Platform") && !is_valid_slug("-a") && !is_valid_slug("a--b") && !is_valid_slug(""));
689 }
690
691 #[test]
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.692 fn leads_are_people() {
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.693 assert_eq!(parse_lead("@Priya"), Some(LeadInput::User("priya".into())));
694 assert_eq!(parse_lead("priya"), Some(LeadInput::User("priya".into())));
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.695 assert_eq!(parse_lead("agent:agt_123"), None);
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.696 assert_eq!(parse_lead("not a name"), None);
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.697 // A lead written as an agent, from before, still parses.
698 let lead: TeamLead = serde_json::from_value(serde_json::json!({ "kind": "agent", "agent_id": "agt_1" })).unwrap();
699 assert_eq!(lead, TeamLead::Agent { agent_id: "agt_1".into() });
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.700 let lead: TeamLead = serde_json::from_value(serde_json::json!({ "kind": "user", "username": "ana" })).unwrap();
701 assert_eq!(lead, TeamLead::User { username: "ana".into(), name: None, avatar: None });
702 }
703
704 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar705 fn handles_name_a_workspace_and_a_team() {
706 assert_eq!(parse_handle("@acme/backend"), Some(("acme".into(), "backend".into())));
707 assert_eq!(parse_handle("@Acme/Backend"), Some(("acme".into(), "backend".into())));
708 assert_eq!(parse_handle("acme/backend"), None);
709 assert_eq!(parse_handle("@acme"), None);
710 assert_eq!(parse_handle("@acme/a/b"), None);
711 }
712
713 #[test]
714 fn words_read_back() {
715 for visibility in [TeamVisibility::Visible, TeamVisibility::Secret] {
716 assert_eq!(TeamVisibility::parse(visibility.as_str()), Some(visibility));
717 assert_eq!(serde_json::to_value(visibility).unwrap(), visibility.as_str());
718 }
719 for role in [TeamRole::Member, TeamRole::Maintainer] {
720 assert_eq!(TeamRole::parse(role.as_str()), Some(role));
721 assert_eq!(serde_json::to_value(role).unwrap(), role.as_str());
722 }
723 for algorithm in [ReviewAlgorithm::RoundRobin, ReviewAlgorithm::LoadBalance] {
724 assert_eq!(ReviewAlgorithm::parse(algorithm.as_str()), Some(algorithm));
725 assert_eq!(serde_json::to_value(algorithm).unwrap(), algorithm.as_str());
726 }
727 assert!(TeamRole::Member < TeamRole::Maintainer);
728 }
729
730 #[test]
731 fn review_assignment_is_kept_within_bounds() {
732 let wild = ReviewAssignment {
733 count: 0,
734 busy_at: 0,
735 excluded: vec!["@Ana".into(), "ana".into(), " ".into(), "bo".into()],
736 ..ReviewAssignment::default()
737 }
738 .bounded();
739 assert_eq!(wild.count, 1);
740 assert_eq!(wild.busy_at, 1);
741 assert_eq!(wild.excluded, vec!["ana", "bo"]);
742 assert_eq!(ReviewAssignment { count: 50, ..ReviewAssignment::default() }.bounded().count, MAX_ASSIGNED);
743 }
744}

This file's history is long; its oldest lines are credited to the oldest commit read.