Skip to content
885 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
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.241/// Who leads a team: a person on it, or an agent on it.
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 },
252 /// An agent, by its id in the agents service.
253 Agent { agent_id: String },
254}
255
256/// A lead as written to `update_team`: `@username` (or a bare username)
257/// for a person, `agent:<id>` for an agent.
258#[derive(Clone, Debug, PartialEq, Eq)]
259pub enum LeadInput {
260 User(String),
261 Agent(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> {
267 let text = text.trim();
268 if let Some(id) = text.strip_prefix("agent:") {
269 let id = id.trim();
270 let ok = !id.is_empty() && id.len() <= 64 && id.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-');
271 return ok.then(|| LeadInput::Agent(id.to_owned()));
272 }
273 let name = text.trim_start_matches('@').to_lowercase();
274 crate::is_valid_namespace(&name).then_some(LeadInput::User(name))
275}
276
277/// A team's chat channel.
278#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
279pub struct TeamChannel {
280 /// Its id in the chat service.
281 pub id: String,
282 /// Its name, without `#`, as it was when chosen.
283 pub name: String,
284}
285
286/// One agent on a team.
287#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
288pub struct TeamAgent {
289 pub agent_id: String,
290 /// Who added it, by username; null when its account is gone.
291 pub added_by: Option<String>,
292 /// RFC 3339.
293 pub created_at: String,
294}
295
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar296/// A team as another names it: its parent, or a child.
297#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
298pub struct TeamRef {
299 pub slug: String,
300 pub name: String,
301}
302
303#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
304pub struct Team {
305 pub id: String,
306 /// Its workspace's slug.
307 pub workspace: String,
308 /// Its name in URLs and mentions: `@<workspace>/<slug>`.
309 pub slug: String,
310 pub name: String,
311 pub description: Option<String>,
312 pub visibility: TeamVisibility,
313 pub parent: Option<TeamRef>,
314 /// Whether its people are notified when it is mentioned.
315 pub notify: bool,
316 pub review_assignment: ReviewAssignment,
317 /// Its own people, not counting child teams'.
318 pub members_count: u32,
319 /// Repositories it has a role on itself, not counting inherited ones.
320 pub repos_count: u32,
321 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.322 /// Agents added to it. Agents whose home team it is are on it too.
323 #[serde(default)]
324 pub agents_count: u32,
325 /// Who leads it, if anyone.
326 #[serde(default)]
327 pub lead: Option<TeamLead>,
328 /// Its chat channel, if it has one.
329 #[serde(default)]
330 pub channel: Option<TeamChannel>,
331 /// What its agents may spend together in a calendar month, in
332 /// millionths of a dollar; null for no team budget.
333 #[serde(default)]
334 pub budget_micros: Option<i64>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar335 /// The viewer's place in it, if any.
336 pub viewer_role: Option<TeamRole>,
337 /// Whether the viewer may change it: an owner of the workspace, or one
338 /// of its maintainers.
339 pub can_manage: bool,
340 /// RFC 3339.
341 pub created_at: String,
342 pub updated_at: String,
343}
344
345impl Team {
346 /// How it is mentioned: `@acme/backend`.
347 pub fn handle(&self) -> String {
348 format!("@{}/{}", self.workspace, self.slug)
349 }
350}
351
352/// One person in a team.
353#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
354pub struct TeamMember {
355 pub username: String,
356 pub name: Option<String>,
357 pub avatar: Option<String>,
358 pub role: TeamRole,
359 /// The child team they are in, when listed through one; null for the
360 /// team's own people.
361 pub via: Option<String>,
362}
363
364/// A repository a team has a role on.
365#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
366pub struct TeamRepo {
367 /// `workspace/name`.
368 pub repo: String,
369 pub repo_id: String,
370 pub role: RepoRole,
371 /// The parent team it comes from, by slug, when the team inherits it.
372 pub inherited_from: Option<String>,
373}
374
375/// A team with a role on a repository, as its Access settings list it.
376#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
377pub struct RepoTeam {
378 pub slug: String,
379 pub name: String,
380 pub role: RepoRole,
381 pub members_count: u32,
382 pub visibility: TeamVisibility,
383}
384
385/// A team as services need it to notify or ask its people: everyone in it,
386/// and its settings. Never shown as is.
387#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
388pub struct ResolvedTeam {
389 pub id: String,
390 pub workspace: String,
391 pub slug: String,
392 pub name: String,
393 pub visibility: TeamVisibility,
394 pub notify: bool,
395 pub review_assignment: ReviewAssignment,
396 /// Its own people.
397 pub members: Vec<TeamPerson>,
398 /// The people of its child teams (and theirs) who are not its own.
399 pub child_members: Vec<TeamPerson>,
400 /// Its role on the repository asked about, its own or inherited.
401 pub repo_role: Option<RepoRole>,
402 /// Whether the person asked about (`asker`) may see it, and so mention
403 /// it or ask it to review: a member of its workspace, and for a secret
404 /// team, in it or an owner.
405 #[serde(default)]
406 pub asker_sees: bool,
407}
408
409impl ResolvedTeam {
410 /// Everyone a mention or a request reaches: its own people, then its
411 /// child teams'.
412 pub fn everyone(&self) -> impl Iterator<Item = &TeamPerson> {
413 self.members.iter().chain(self.child_members.iter())
414 }
415
416 pub fn handle(&self) -> String {
417 format!("@{}/{}", self.workspace, self.slug)
418 }
419}
420
421#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
422pub struct TeamPerson {
423 pub id: String,
424 pub username: String,
425}
426
427/// A team's slug from its name: lowercase letters, digits and single
428/// hyphens, as mentions spell it. `None` when nothing is left.
429pub fn slug_of(name: &str) -> Option<String> {
430 let mut slug = String::new();
431 for c in name.trim().chars() {
432 if c.is_ascii_alphanumeric() {
433 slug.push(c.to_ascii_lowercase());
434 } else if !slug.is_empty() && !slug.ends_with('-') {
435 slug.push('-');
436 }
437 }
438 let slug = slug.trim_end_matches('-').chars().take(60).collect::<String>();
439 let slug = slug.trim_end_matches('-').to_owned();
440 is_valid_slug(&slug).then_some(slug)
441}
442
443/// Whether `slug` is a team slug: 1 to 60 lowercase letters, digits and
444/// single hyphens, not starting or ending with one.
445pub fn is_valid_slug(slug: &str) -> bool {
446 !slug.is_empty()
447 && slug.len() <= 60
448 && slug.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
449 && !slug.starts_with('-')
450 && !slug.ends_with('-')
451 && !slug.contains("--")
452}
453
454/// `@workspace/team` as written, split; `None` if it is not that shape.
455pub fn parse_handle(text: &str) -> Option<(String, String)> {
456 let text = text.trim().strip_prefix('@')?;
457 let (workspace, slug) = text.split_once('/')?;
458 let workspace = workspace.to_lowercase();
459 let slug = slug.to_lowercase();
460 (crate::is_valid_namespace(&workspace) && is_valid_slug(&slug)).then_some((workspace, slug))
461}
462
463// --- Identity methods ---------------------------------------------------------
464
465/// `list_teams`: the teams of a workspace the viewer can see, theirs first,
466/// then by name. Members only. `query` narrows by name or slug. Returns
467/// `Outcome<Vec<Team>>`.
468#[derive(Debug, Serialize, Deserialize)]
469pub struct ListTeamsArgs {
470 pub viewer: crate::Viewer,
471 pub workspace: String,
472 #[serde(default)]
473 pub query: Option<String>,
474}
475
476/// `get_team`, `child_teams`, `team_repos`: one team, its child teams, or
477/// the repositories it has a role on (its own and inherited), as the
478/// viewer may see them. `team_members` takes `include_child_teams`.
479#[derive(Debug, Serialize, Deserialize)]
480pub struct TeamArgs {
481 pub viewer: crate::Viewer,
482 pub workspace: String,
483 pub team: String,
484 /// `team_members` only: also list the people of child teams.
485 #[serde(default)]
486 pub include_child_teams: bool,
487}
488
Merge branch 'worktree-agent-ad7c6d88d93adc817'489/// `create_team`. Members may create a team, unless the workspace's
490/// [`TeamCreation`] says owners only, and become its maintainer; a team
491/// with a parent needs an owner, or a maintainer of the parent. Returns
492/// `Outcome<Team>`.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar493#[derive(Debug, Serialize, Deserialize)]
494pub struct CreateTeamArgs {
495 pub actor: User,
496 pub workspace: String,
497 pub name: String,
498 /// Defaults to one made from the name.
499 #[serde(default)]
500 pub slug: Option<String>,
501 #[serde(default)]
502 pub description: Option<String>,
503 #[serde(default)]
504 pub visibility: Option<TeamVisibility>,
505 /// The parent's slug.
506 #[serde(default)]
507 pub parent: Option<String>,
508 #[serde(default)]
509 pub notify: Option<bool>,
510 /// People to add as members, by username, besides the creator.
511 #[serde(default)]
512 pub members: Vec<String>,
513 #[serde(default)]
514 pub surface: Option<crate::audit::Surface>,
515}
516
517/// `update_team`: what is given changes; the rest stays. `parent` set to an
518/// empty string takes the team out from under its parent. Owners and the
519/// team's maintainers. Returns `Outcome<Team>`.
520#[derive(Debug, Default, Serialize, Deserialize)]
521pub struct UpdateTeamArgs {
522 pub actor: User,
523 pub workspace: String,
524 pub team: String,
525 #[serde(default)]
526 pub name: Option<String>,
527 #[serde(default)]
528 pub slug: Option<String>,
529 #[serde(default)]
530 pub description: Option<String>,
531 #[serde(default)]
532 pub visibility: Option<TeamVisibility>,
533 #[serde(default)]
534 pub parent: Option<String>,
535 #[serde(default)]
536 pub notify: Option<bool>,
537 #[serde(default)]
538 pub review_assignment: Option<ReviewAssignment>,
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.539 /// Who leads it: `@username` or `agent:<id>`, someone on the team; an
540 /// empty string for no lead.
541 #[serde(default)]
542 pub lead: Option<String>,
543 /// Its chat channel's id in the chat service; an empty string for none.
544 #[serde(default)]
545 pub channel_id: Option<String>,
546 /// That channel's name, without `#`; given with `channel_id`.
547 #[serde(default)]
548 pub channel_name: Option<String>,
549 /// What its agents may spend together in a month, in millionths of a
550 /// dollar; 0 for no team budget.
551 #[serde(default)]
552 pub budget_micros: Option<i64>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar553 #[serde(default)]
554 pub surface: Option<crate::audit::Surface>,
555}
556
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.557/// `set_team_agent`: adds one of the workspace's agents to a team. The
558/// caller checks the agent is the workspace's (identity knows agents only
559/// by id). Owners and the team's maintainers. Returns `Outcome<TeamAgent>`.
560#[derive(Debug, Serialize, Deserialize)]
561pub struct SetTeamAgentArgs {
562 pub actor: User,
563 pub workspace: String,
564 pub team: String,
565 pub agent_id: String,
566 #[serde(default)]
567 pub surface: Option<crate::audit::Surface>,
568}
569
570/// `remove_team_agent`: takes an agent off a team; if it led the team,
571/// the team has no lead after. Owners and the team's maintainers.
572/// Returns `Outcome<bool>`.
573#[derive(Debug, Serialize, Deserialize)]
574pub struct RemoveTeamAgentArgs {
575 pub actor: User,
576 pub workspace: String,
577 pub team: String,
578 pub agent_id: String,
579 #[serde(default)]
580 pub surface: Option<crate::audit::Surface>,
581}
582
583/// `agent_teams`: for the agents service. The visible teams an agent is
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.584/// on in a workspace (its `team_agents` memberships), each with everyone
585/// on it, for what the agent is told every turn and its team budgets.
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.586/// Returns `Vec<AgentTeam>`.
587#[derive(Debug, Default, Serialize, Deserialize)]
588pub struct AgentTeamsArgs {
589 /// The workspace's slug.
590 pub workspace: String,
591 pub agent_id: String,
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.592 /// Sent only by an agents service from before agents' teams were
593 /// memberships, while a deploy rolls out: a team it named itself, by
594 /// slug, counted as one it is on. Newer callers leave it out.
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.595 #[serde(default)]
596 pub home_team: Option<String>,
597}
598
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.599/// `team_agent_index`: for the agents service. A workspace's visible
600/// teams, each with the agents on it, for the roster agents are told and
601/// spend by team. Returns `Vec<TeamAgentsEntry>`.
602#[derive(Debug, Default, Serialize, Deserialize)]
603pub struct TeamAgentIndexArgs {
604 /// The workspace's slug.
605 pub workspace: String,
606}
607
608/// A visible team and the agents on it, by id.
609#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
610pub struct TeamAgentsEntry {
611 pub slug: String,
612 pub name: String,
613 pub agent_ids: Vec<String>,
614}
615
616/// `adopt_agent_teams`: for the agents service, once. Puts each agent on
617/// the team it named itself when an agent carried its own team, matched
618/// by slug in any case, in the workspace by id. Safe to repeat: an agent
619/// already on the team stays as it is. Returns `Vec<AdoptedAgentTeam>`.
620#[derive(Debug, Default, Serialize, Deserialize)]
621pub struct AdoptAgentTeamsArgs {
622 pub workspace_id: String,
623 pub agents: Vec<AgentTeamClaim>,
624}
625
626/// An agent and the team it named, by slug.
627#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
628pub struct AgentTeamClaim {
629 pub agent_id: String,
630 pub team: String,
631}
632
633/// What `adopt_agent_teams` did for one agent.
634#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
635pub struct AdoptedAgentTeam {
636 pub agent_id: String,
637 pub team: String,
638 /// `added`, `already` (on it before), or `no_team` (no team of that name).
639 pub outcome: String,
640}
641
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.642/// A person on a team, as an agent on it is told of them.
643#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
644pub struct RosterPerson {
645 pub user_id: String,
646 pub username: String,
647 pub name: Option<String>,
648 pub title: Option<String>,
649 pub timezone: Option<String>,
650 pub owns: Vec<String>,
651 /// Who they report to, by username.
652 pub manager: Option<String>,
653 pub maintainer: bool,
654}
655
656/// A team an agent is on, as it is told of it.
657#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
658pub struct AgentTeam {
659 pub slug: String,
660 pub name: String,
661 pub description: Option<String>,
662 pub lead: Option<TeamLead>,
663 pub channel: Option<TeamChannel>,
664 pub budget_micros: Option<i64>,
665 pub people: Vec<RosterPerson>,
666 /// The agents added to it, by id.
667 pub agent_ids: Vec<String>,
668}
669
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar670/// `delete_team`: its child teams move up to its parent, and the roles it
671/// gave go with it. Owners and the team's maintainers. Returns
672/// `Outcome<bool>`.
673#[derive(Debug, Serialize, Deserialize)]
674pub struct DeleteTeamArgs {
675 pub actor: User,
676 pub workspace: String,
677 pub team: String,
678 #[serde(default)]
679 pub surface: Option<crate::audit::Surface>,
680}
681
682/// `set_team_member`: adds a member of the workspace to a team, or changes
683/// their role in it. Owners and the team's maintainers. Returns
684/// `Outcome<TeamMember>`.
685#[derive(Debug, Serialize, Deserialize)]
686pub struct SetTeamMemberArgs {
687 pub actor: User,
688 pub workspace: String,
689 pub team: String,
690 pub username: String,
691 pub role: TeamRole,
692 #[serde(default)]
693 pub surface: Option<crate::audit::Surface>,
694}
695
696/// `remove_team_member`: owners and the team's maintainers; anyone may
697/// leave a team themselves. Returns `Outcome<bool>`.
698#[derive(Debug, Serialize, Deserialize)]
699pub struct RemoveTeamMemberArgs {
700 pub actor: User,
701 pub workspace: String,
702 pub team: String,
703 pub username: String,
704 #[serde(default)]
705 pub surface: Option<crate::audit::Surface>,
706}
707
708/// `set_team_repo`: gives a team a role on a repository of its workspace,
709/// or changes it. Needs Admin on the repository. Returns
710/// `Outcome<TeamRepo>`.
711#[derive(Debug, Serialize, Deserialize)]
712pub struct SetTeamRepoArgs {
713 pub actor: User,
714 pub workspace: String,
715 pub team: String,
716 pub repo: RepoPath,
717 pub role: RepoRole,
718 #[serde(default)]
719 pub surface: Option<crate::audit::Surface>,
720}
721
722/// `remove_team_repo`: takes a team's role on a repository away. Admin on
723/// the repository, an owner, or one of the team's maintainers. Returns
724/// `Outcome<bool>`.
725#[derive(Debug, Serialize, Deserialize)]
726pub struct RemoveTeamRepoArgs {
727 pub actor: User,
728 pub workspace: String,
729 pub team: String,
730 pub repo: RepoPath,
731 #[serde(default)]
732 pub surface: Option<crate::audit::Surface>,
733}
734
735/// `user_teams`: the teams `username` is in within a workspace, as the
736/// viewer may see them. Members only. Returns `Outcome<Vec<Team>>`.
737#[derive(Debug, Serialize, Deserialize)]
738pub struct UserTeamsArgs {
739 pub viewer: crate::Viewer,
740 pub workspace: String,
741 pub username: String,
742}
743
744/// `team_memberships`: for each member of a workspace, the teams they are
745/// in that the viewer can see, for the Members page. Members only.
746/// Returns `Outcome<Vec<MemberTeams>>`.
747#[derive(Debug, Serialize, Deserialize)]
748pub struct TeamMembershipsArgs {
749 pub viewer: crate::Viewer,
750 pub workspace: String,
751}
752
753/// One person's teams in a workspace.
754#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
755pub struct MemberTeams {
756 pub username: String,
757 pub teams: Vec<TeamRef>,
758}
759
760/// `resolve_teams`: for services. Each team named `workspace/slug` (or
761/// `@workspace/slug`) that exists, with everyone in it and, given
762/// `repo_id`, its role on that repository. Missing teams are left out.
763/// Returns `Vec<ResolvedTeam>`.
764#[derive(Debug, Default, Serialize, Deserialize)]
765pub struct ResolveTeamsArgs {
766 pub teams: Vec<String>,
767 #[serde(default)]
768 pub repo_id: Option<String>,
769 /// A user id, for `asker_sees`.
770 #[serde(default)]
771 pub asker: Option<String>,
772}
773
774/// One owner a CODEOWNERS file names, as identity resolved it.
775#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
776pub struct ResolvedOwner {
777 pub owner: crate::codeowners::Owner,
778 pub check: crate::codeowners::OwnerCheck,
779 /// Who may answer for it, by username: the person, the account a
780 /// confirmed address belongs to, or everyone in a team and its child
781 /// teams. Empty when it did not resolve.
782 pub members: Vec<String>,
783 /// For a team: the team, as `workspace/slug`, after `@org/team` was
784 /// mapped to the repository's workspace.
785 #[serde(default)]
786 pub team: Option<String>,
787}
788
789/// `resolve_owners`: for services. Resolves the owners a CODEOWNERS file
790/// names on a repository: accounts, teams of the repository's workspace
791/// (`@org/team` with an `org` that is not a g1t workspace means the
792/// repository's own workspace), and confirmed email addresses, checking
793/// each has the Write role or higher on it. `g1t` resolves to itself.
794/// Returns `Vec<ResolvedOwner>`, in the order asked.
795#[derive(Debug, Serialize, Deserialize)]
796pub struct ResolveOwnersArgs {
797 pub repo_id: String,
798 /// The repository's workspace, by slug.
799 pub workspace: String,
800 pub owners: Vec<crate::codeowners::Owner>,
801}
802
803#[cfg(test)]
804mod tests {
805 use super::*;
806
807 #[test]
Merge branch 'worktree-agent-ad7c6d88d93adc817'808 fn who_may_create_teams() {
809 assert_eq!(TeamCreation::default(), TeamCreation::Members);
810 assert!(TeamCreation::Members.allows(Role::Member) && TeamCreation::Members.allows(Role::Owner));
811 assert!(!TeamCreation::Owners.allows(Role::Member) && TeamCreation::Owners.allows(Role::Owner));
812 for setting in TeamCreation::ALL {
813 assert_eq!(TeamCreation::parse(setting.as_str()), Some(setting));
814 assert_eq!(serde_json::to_value(setting).unwrap(), setting.as_str());
815 }
816 assert_eq!(TeamCreation::parse(" Owners "), Some(TeamCreation::Owners));
817 assert_eq!(TeamCreation::parse("maintainers"), None);
818 }
819
820 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar821 fn slugs_come_from_names() {
822 assert_eq!(slug_of("Backend").as_deref(), Some("backend"));
823 assert_eq!(slug_of(" Web & Mobile ").as_deref(), Some("web-mobile"));
824 assert_eq!(slug_of("SRE / On-call").as_deref(), Some("sre-on-call"));
825 assert_eq!(slug_of("!!!"), None);
826 assert_eq!(slug_of(&"a".repeat(80)).map(|slug| slug.len()), Some(60));
827 assert!(is_valid_slug("platform-2"));
828 assert!(!is_valid_slug("Platform") && !is_valid_slug("-a") && !is_valid_slug("a--b") && !is_valid_slug(""));
829 }
830
831 #[test]
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.832 fn leads_are_people_or_agents() {
833 assert_eq!(parse_lead("@Priya"), Some(LeadInput::User("priya".into())));
834 assert_eq!(parse_lead("priya"), Some(LeadInput::User("priya".into())));
835 assert_eq!(parse_lead("agent:agt_123"), Some(LeadInput::Agent("agt_123".into())));
836 assert_eq!(parse_lead("agent:"), None);
837 assert_eq!(parse_lead("agent:a b"), None);
838 assert_eq!(parse_lead("not a name"), None);
839 let lead = TeamLead::Agent { agent_id: "agt_1".into() };
840 assert_eq!(serde_json::to_value(&lead).unwrap(), serde_json::json!({ "kind": "agent", "agent_id": "agt_1" }));
841 let lead: TeamLead = serde_json::from_value(serde_json::json!({ "kind": "user", "username": "ana" })).unwrap();
842 assert_eq!(lead, TeamLead::User { username: "ana".into(), name: None, avatar: None });
843 }
844
845 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar846 fn handles_name_a_workspace_and_a_team() {
847 assert_eq!(parse_handle("@acme/backend"), Some(("acme".into(), "backend".into())));
848 assert_eq!(parse_handle("@Acme/Backend"), Some(("acme".into(), "backend".into())));
849 assert_eq!(parse_handle("acme/backend"), None);
850 assert_eq!(parse_handle("@acme"), None);
851 assert_eq!(parse_handle("@acme/a/b"), None);
852 }
853
854 #[test]
855 fn words_read_back() {
856 for visibility in [TeamVisibility::Visible, TeamVisibility::Secret] {
857 assert_eq!(TeamVisibility::parse(visibility.as_str()), Some(visibility));
858 assert_eq!(serde_json::to_value(visibility).unwrap(), visibility.as_str());
859 }
860 for role in [TeamRole::Member, TeamRole::Maintainer] {
861 assert_eq!(TeamRole::parse(role.as_str()), Some(role));
862 assert_eq!(serde_json::to_value(role).unwrap(), role.as_str());
863 }
864 for algorithm in [ReviewAlgorithm::RoundRobin, ReviewAlgorithm::LoadBalance] {
865 assert_eq!(ReviewAlgorithm::parse(algorithm.as_str()), Some(algorithm));
866 assert_eq!(serde_json::to_value(algorithm).unwrap(), algorithm.as_str());
867 }
868 assert!(TeamRole::Member < TeamRole::Maintainer);
869 }
870
871 #[test]
872 fn review_assignment_is_kept_within_bounds() {
873 let wild = ReviewAssignment {
874 count: 0,
875 busy_at: 0,
876 excluded: vec!["@Ana".into(), "ana".into(), " ".into(), "bo".into()],
877 ..ReviewAssignment::default()
878 }
879 .bounded();
880 assert_eq!(wild.count, 1);
881 assert_eq!(wild.busy_at, 1);
882 assert_eq!(wild.excluded, vec!["ana", "bo"]);
883 assert_eq!(ReviewAssignment { count: 50, ..ReviewAssignment::default() }.bounded().count, MAX_ASSIGNED);
884 }
885}

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