Skip to content
261 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 /** Who they report to, by username. */
99 manager: string | null;
100 maintainer: boolean;
101};
102
103/** A visible team an agent is on, with everyone on it. For the agents service. */
104export type AgentTeam = {
105 slug: string;
106 name: string;
107 description: string | null;
108 lead: TeamLead | null;
109 channel: TeamChannel | null;
110 budget_micros: number | null;
111 people: RosterPerson[];
112 /** Agents added to it, by id. */
113 agent_ids: string[];
114};
115
116/** A visible team and the agents on it (`team_agent_index`). */
117export type TeamAgentsEntry = { slug: string; name: string; agent_ids: string[] };
118
119/** What `adopt_agent_teams` did for one agent: `added`, `already` on it, or `no_team` of that name. */
120export type AdoptedAgentTeam = { agent_id: string; team: string; outcome: "added" | "already" | "no_team" };
121
122/** How `update_team` takes a lead: `@username`, or `agent:<id>`; `""` for none. */
123export function leadInput(lead: { kind: "user"; username: string } | { kind: "agent"; agent_id: string } | null): string {
124 if (!lead) return "";
125 return lead.kind === "user" ? `@${lead.username}` : `agent:${lead.agent_id}`;
126}
127
128export type Team = {
129 id: string;
130 workspace: string;
131 slug: string;
132 name: string;
133 description: string | null;
134 visibility: TeamVisibility;
135 parent: TeamRef | null;
136 notify: boolean;
137 review_assignment: ReviewAssignment;
138 members_count: number;
139 repos_count: number;
140 child_teams_count: number;
141 /** Agents on it. */
142 agents_count: number;
143 lead: TeamLead | null;
144 channel: TeamChannel | null;
145 /** What its agents may spend together in a calendar month, in micro-dollars; null for none. */
146 budget_micros: number | null;
147 viewer_role: TeamRole | null;
148 can_manage: boolean;
149 created_at: string;
150 updated_at: string;
151};
152
153export type TeamMember = {
154 username: string;
155 name: string | null;
156 avatar: string | null;
157 role: TeamRole;
158 /** The child team they are in, when listed through one. */
159 via: string | null;
160};
161
162export type TeamRepo = {
163 /** `workspace/name`. */
164 repo: string;
165 repo_id: string;
166 role: RepoRole;
167 /** The parent team it comes from, when inherited. */
168 inherited_from: string | null;
169};
170
171/** A team with a role on a repository, as its Access settings list it. */
172export type RepoTeam = {
173 slug: string;
174 name: string;
175 role: RepoRole;
176 members_count: number;
177 visibility: TeamVisibility;
178};
179
180export type NewTeam = {
181 name: string;
182 slug?: string | null;
183 description?: string | null;
184 visibility?: TeamVisibility | null;
185 parent?: string | null;
186 notify?: boolean | null;
187 members?: string[];
188};
189
190/** What changes; the rest stays. `parent: ""` takes the team out from under its parent. */
191export type TeamChanges = {
192 name?: string;
193 slug?: string;
194 description?: string;
195 visibility?: TeamVisibility;
196 parent?: string;
197 notify?: boolean;
198 review_assignment?: ReviewAssignment;
199 /** `@username` or `agent:<id>`, someone on the team (see `leadInput`); `""` for no lead. */
200 lead?: string;
201 /** The chat channel's id; `""` for none. Given with `channel_name`. */
202 channel_id?: string;
203 channel_name?: string;
204 /** What its agents may spend together in a month, in micro-dollars; 0 for no team budget. */
205 budget_micros?: number;
206};
207
208/** `@acme/backend`, the way a team is mentioned. */
209export function teamHandle(team: { workspace: string; slug: string }): string {
210 return `@${team.workspace}/${team.slug}`;
211}
212
213/** A team's slug from its name, as identity makes it. Null when nothing is left. */
214export function teamSlug(name: string): string | null {
215 let slug = "";
216 for (const c of name.trim()) {
217 if (/[A-Za-z0-9]/.test(c)) slug += c.toLowerCase();
218 else if (slug && !slug.endsWith("-")) slug += "-";
219 }
220 slug = slug.replace(/-+$/, "").slice(0, 60).replace(/-+$/, "");
221 return slug ? slug : null;
222}
223
224/** One person's teams in a workspace, for the Members page. */
225export type MemberTeams = { username: string; teams: TeamRef[] };
226
227/** Identity's team methods. */
228export interface TeamsClient {
229 listTeams(viewer: User | null, workspace: string, query?: string | null): Promise<Result<Team[]>>;
230 getTeam(viewer: User | null, workspace: string, team: string): Promise<Result<Team>>;
231 createTeam(actor: User, workspace: string, team: NewTeam): Promise<Result<Team>>;
232 /** Who may create the workspace's teams. Owners only. */
233 setTeamCreation(actor: User, slug: string, setting: TeamCreation): Promise<Result<TeamCreation>>;
234 updateTeam(actor: User, workspace: string, team: string, changes: TeamChanges): Promise<Result<Team>>;
235 deleteTeam(actor: User, workspace: string, team: string): Promise<Result<boolean>>;
236 teamMembers(viewer: User | null, workspace: string, team: string, includeChildTeams?: boolean): Promise<Result<TeamMember[]>>;
237 setTeamMember(actor: User, workspace: string, team: string, username: string, role: TeamRole): Promise<Result<TeamMember>>;
238 removeTeamMember(actor: User, workspace: string, team: string, username: string): Promise<Result<boolean>>;
239 childTeams(viewer: User | null, workspace: string, team: string): Promise<Result<Team[]>>;
240 teamRepos(viewer: User | null, workspace: string, team: string): Promise<Result<TeamRepo[]>>;
241 setTeamRepo(actor: User, workspace: string, team: string, owner: string, name: string, role: RepoRole): Promise<Result<TeamRepo>>;
242 removeTeamRepo(actor: User, workspace: string, team: string, owner: string, name: string): Promise<Result<boolean>>;
243 /** The agents on a team: added to it, as people are. */
244 teamAgents(viewer: User | null, workspace: string, team: string): Promise<Result<TeamAgent[]>>;
245 /** Adds one of the workspace's agents, by id; check it is the workspace's first. Owners and maintainers. */
246 setTeamAgent(actor: User, workspace: string, team: string, agentId: string): Promise<Result<TeamAgent>>;
247 removeTeamAgent(actor: User, workspace: string, team: string, agentId: string): Promise<Result<boolean>>;
248 /** For the agents service: the visible teams an agent is on, with everyone on each. */
249 agentTeams(workspace: string, agentId: string): Promise<AgentTeam[]>;
250 /** For the agents service: the workspace's visible teams, each with the agents on it. */
251 teamAgentIndex(workspace: string): Promise<TeamAgentsEntry[]>;
252 /**
253 * For the agents service, once: puts agents on the teams they named when
254 * an agent carried its own team (by slug, any case), in the workspace
255 * (by id). Safe to repeat: an agent already on the team stays as it is.
256 */
257 adoptAgentTeams(workspaceId: string, agents: { agent_id: string; team: string }[]): Promise<AdoptedAgentTeam[]>;
258 userTeams(viewer: User | null, workspace: string, username: string): Promise<Result<Team[]>>;
259 /** Each member's teams the viewer can see. Members only. */
260 teamMemberships(viewer: User | null, workspace: string): Promise<Result<MemberTeams[]>>;
261}