Skip to content
259 linesCodeBlameRaw
1/**
2 * Teams: groups of a workspace's members, given roles on repositories
3 * together, mentioned together and asked to review together. Kept by the
4 * identity service; mirrors `crates/contracts/src/teams.rs`.
5 */
6import type { RepoRole } from "./access";
7import type { User } from "./identity";
8import type { Result } from "./result";
9
10export type TeamVisibility = "visible" | "secret";
11
12/**
13 * Who may create a workspace's teams: any member (the default) or its
14 * owners only. Owners change it in the workspace's settings.
15 */
16export type TeamCreation = "members" | "owners";
17
18/** Whether someone with `role` in a workspace may create its teams. */
19export function mayCreateTeams(setting: TeamCreation | null | undefined, role: "owner" | "member" | null | undefined): boolean {
20 if (!role) return false;
21 return (setting ?? "members") === "members" || role === "owner";
22}
23export type TeamRole = "member" | "maintainer";
24export type ReviewAlgorithm = "round_robin" | "load_balance";
25
26export const TEAM_VISIBILITY_LABELS: Record<TeamVisibility, string> = {
27 visible: "Visible",
28 secret: "Secret",
29};
30
31export const TEAM_VISIBILITY_SUMMARIES: Record<TeamVisibility, string> = {
32 visible: "Every member of the workspace can see it and mention it.",
33 secret: "Only its own people and the workspace's owners can see it. Secret teams cannot be nested.",
34};
35
36export const REVIEW_ALGORITHM_LABELS: Record<ReviewAlgorithm, string> = {
37 round_robin: "Round robin",
38 load_balance: "Load balance",
39};
40
41export const REVIEW_ALGORITHM_SUMMARIES: Record<ReviewAlgorithm, string> = {
42 round_robin: "Whoever this team asked least recently goes first.",
43 load_balance: "Whoever has the fewest pull requests waiting on their review goes first.",
44};
45
46/** The most people review assignment picks for one request. */
47export const MAX_ASSIGNED = 10;
48
49export type ReviewAssignment = {
50 enabled: boolean;
51 algorithm: ReviewAlgorithm;
52 count: number;
53 skip_busy: boolean;
54 busy_at: number;
55 include_child_teams: boolean;
56 excluded: string[];
57 notify_team: boolean;
58};
59
60export const DEFAULT_REVIEW_ASSIGNMENT: ReviewAssignment = {
61 enabled: false,
62 algorithm: "round_robin",
63 count: 1,
64 skip_busy: false,
65 busy_at: 5,
66 include_child_teams: false,
67 excluded: [],
68 notify_team: false,
69};
70
71export type TeamRef = { slug: string; name: string };
72
73/** Who leads a team: a person on it, or an agent on it (by its id in the agents service). */
74export type TeamLead =
75 | { kind: "user"; username: string; name: string | null; avatar: string | null }
76 | { kind: "agent"; agent_id: string };
77
78/** A team's chat channel: its id, and its name (without `#`) when it was chosen. */
79export type TeamChannel = { id: string; name: string };
80
81/** An agent added to a team. */
82export type TeamAgent = {
83 agent_id: string;
84 /** Who added it, by username. */
85 added_by: string | null;
86 created_at: string;
87};
88
89/** A person on a team, as an agent on it is told of them (`agent_teams`). */
90export type RosterPerson = {
91 user_id: string;
92 username: string;
93 name: string | null;
94 title: string | null;
95 /** An IANA time zone, for their local time. */
96 timezone: string | null;
97 owns: string[];
98 maintainer: boolean;
99};
100
101/** A visible team an agent is on, with everyone on it. For the agents service. */
102export type AgentTeam = {
103 slug: string;
104 name: string;
105 description: string | null;
106 lead: TeamLead | null;
107 channel: TeamChannel | null;
108 budget_micros: number | null;
109 people: RosterPerson[];
110 /** Agents added to it, by id. */
111 agent_ids: string[];
112};
113
114/** A visible team and the agents on it (`team_agent_index`). */
115export type TeamAgentsEntry = { slug: string; name: string; agent_ids: string[] };
116
117/** What `adopt_agent_teams` did for one agent: `added`, `already` on it, or `no_team` of that name. */
118export type AdoptedAgentTeam = { agent_id: string; team: string; outcome: "added" | "already" | "no_team" };
119
120/** How `update_team` takes a lead: `@username`, or `agent:<id>`; `""` for none. */
121export function leadInput(lead: { kind: "user"; username: string } | { kind: "agent"; agent_id: string } | null): string {
122 if (!lead) return "";
123 return lead.kind === "user" ? `@${lead.username}` : `agent:${lead.agent_id}`;
124}
125
126export type Team = {
127 id: string;
128 workspace: string;
129 slug: string;
130 name: string;
131 description: string | null;
132 visibility: TeamVisibility;
133 parent: TeamRef | null;
134 notify: boolean;
135 review_assignment: ReviewAssignment;
136 members_count: number;
137 repos_count: number;
138 child_teams_count: number;
139 /** Agents on it. */
140 agents_count: number;
141 lead: TeamLead | null;
142 channel: TeamChannel | null;
143 /** What its agents may spend together in a calendar month, in micro-dollars; null for none. */
144 budget_micros: number | null;
145 viewer_role: TeamRole | null;
146 can_manage: boolean;
147 created_at: string;
148 updated_at: string;
149};
150
151export type TeamMember = {
152 username: string;
153 name: string | null;
154 avatar: string | null;
155 role: TeamRole;
156 /** The child team they are in, when listed through one. */
157 via: string | null;
158};
159
160export type TeamRepo = {
161 /** `workspace/name`. */
162 repo: string;
163 repo_id: string;
164 role: RepoRole;
165 /** The parent team it comes from, when inherited. */
166 inherited_from: string | null;
167};
168
169/** A team with a role on a repository, as its Access settings list it. */
170export type RepoTeam = {
171 slug: string;
172 name: string;
173 role: RepoRole;
174 members_count: number;
175 visibility: TeamVisibility;
176};
177
178export type NewTeam = {
179 name: string;
180 slug?: string | null;
181 description?: string | null;
182 visibility?: TeamVisibility | null;
183 parent?: string | null;
184 notify?: boolean | null;
185 members?: string[];
186};
187
188/** What changes; the rest stays. `parent: ""` takes the team out from under its parent. */
189export type TeamChanges = {
190 name?: string;
191 slug?: string;
192 description?: string;
193 visibility?: TeamVisibility;
194 parent?: string;
195 notify?: boolean;
196 review_assignment?: ReviewAssignment;
197 /** `@username` or `agent:<id>`, someone on the team (see `leadInput`); `""` for no lead. */
198 lead?: string;
199 /** The chat channel's id; `""` for none. Given with `channel_name`. */
200 channel_id?: string;
201 channel_name?: string;
202 /** What its agents may spend together in a month, in micro-dollars; 0 for no team budget. */
203 budget_micros?: number;
204};
205
206/** `@acme/backend`, the way a team is mentioned. */
207export function teamHandle(team: { workspace: string; slug: string }): string {
208 return `@${team.workspace}/${team.slug}`;
209}
210
211/** A team's slug from its name, as identity makes it. Null when nothing is left. */
212export function teamSlug(name: string): string | null {
213 let slug = "";
214 for (const c of name.trim()) {
215 if (/[A-Za-z0-9]/.test(c)) slug += c.toLowerCase();
216 else if (slug && !slug.endsWith("-")) slug += "-";
217 }
218 slug = slug.replace(/-+$/, "").slice(0, 60).replace(/-+$/, "");
219 return slug ? slug : null;
220}
221
222/** One person's teams in a workspace, for the Members page. */
223export type MemberTeams = { username: string; teams: TeamRef[] };
224
225/** Identity's team methods. */
226export interface TeamsClient {
227 listTeams(viewer: User | null, workspace: string, query?: string | null): Promise<Result<Team[]>>;
228 getTeam(viewer: User | null, workspace: string, team: string): Promise<Result<Team>>;
229 createTeam(actor: User, workspace: string, team: NewTeam): Promise<Result<Team>>;
230 /** Who may create the workspace's teams. Owners only. */
231 setTeamCreation(actor: User, slug: string, setting: TeamCreation): Promise<Result<TeamCreation>>;
232 updateTeam(actor: User, workspace: string, team: string, changes: TeamChanges): Promise<Result<Team>>;
233 deleteTeam(actor: User, workspace: string, team: string): Promise<Result<boolean>>;
234 teamMembers(viewer: User | null, workspace: string, team: string, includeChildTeams?: boolean): Promise<Result<TeamMember[]>>;
235 setTeamMember(actor: User, workspace: string, team: string, username: string, role: TeamRole): Promise<Result<TeamMember>>;
236 removeTeamMember(actor: User, workspace: string, team: string, username: string): Promise<Result<boolean>>;
237 childTeams(viewer: User | null, workspace: string, team: string): Promise<Result<Team[]>>;
238 teamRepos(viewer: User | null, workspace: string, team: string): Promise<Result<TeamRepo[]>>;
239 setTeamRepo(actor: User, workspace: string, team: string, owner: string, name: string, role: RepoRole): Promise<Result<TeamRepo>>;
240 removeTeamRepo(actor: User, workspace: string, team: string, owner: string, name: string): Promise<Result<boolean>>;
241 /** The agents on a team: added to it, as people are. */
242 teamAgents(viewer: User | null, workspace: string, team: string): Promise<Result<TeamAgent[]>>;
243 /** Adds one of the workspace's agents, by id; check it is the workspace's first. Owners and maintainers. */
244 setTeamAgent(actor: User, workspace: string, team: string, agentId: string): Promise<Result<TeamAgent>>;
245 removeTeamAgent(actor: User, workspace: string, team: string, agentId: string): Promise<Result<boolean>>;
246 /** For the agents service: the visible teams an agent is on, with everyone on each. */
247 agentTeams(workspace: string, agentId: string): Promise<AgentTeam[]>;
248 /** For the agents service: the workspace's visible teams, each with the agents on it. */
249 teamAgentIndex(workspace: string): Promise<TeamAgentsEntry[]>;
250 /**
251 * For the agents service, once: puts agents on the teams they named when
252 * an agent carried its own team (by slug, any case), in the workspace
253 * (by id). Safe to repeat: an agent already on the team stays as it is.
254 */
255 adoptAgentTeams(workspaceId: string, agents: { agent_id: string; team: string }[]): Promise<AdoptedAgentTeam[]>;
256 userTeams(viewer: User | null, workspace: string, username: string): Promise<Result<Team[]>>;
257 /** Each member's teams the viewer can see. Members only. */
258 teamMemberships(viewer: User | null, workspace: string): Promise<Result<MemberTeams[]>>;
259}