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.
| 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. | 1 | /** |
| People lists people. The directory, its counts and its search cover the workspace's people only; agents keep their page under Agents, where the Profile tab now also says what each agent is told about its teams, and the old address of an agent under People sends you there. Agents still join teams beside people: a team's members page groups them under its people with an agent badge, and the org chart shows them as members of the team beside its lead. The people and teams, agents, teams and workspaces guides say so. | 2 | * People on the site: the directory (people only; agents are on their |
| 3 | * teams and under Agents), profiles, teams of any mix, and the org chart. | |
| 4 | * Types only from the contracts, so this file runs under `node --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. | 5 | */ |
| 6 | import type { DirectoryPerson, DirectoryTeam, PeopleDirectory, TeamLead, WorkspaceAgent } from "@g1t/contracts"; | |
| 7 | ||
| 8 | /** An agent as People's pages need it. */ | |
| 9 | export type PeopleAgent = Pick< | |
| 10 | WorkspaceAgent, | |
| Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent. | 11 | "id" | "handle" | "display_name" | "avatar" | "avatar_seed" | "look" | "title" | "role" | "status" | "responsibilities" | "builtin" | "spent_month_micros" |
| 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. | 12 | >; |
| 13 | ||
| 14 | /** Just what People's pages read of an agent, so loaders send no more. */ | |
| 15 | export function peopleAgent(agent: PeopleAgent): PeopleAgent { | |
| Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent. | 16 | const { id, handle, display_name, avatar, avatar_seed, look, title, role, status, responsibilities, builtin, spent_month_micros } = agent; |
| 17 | return { id, handle, display_name, avatar, avatar_seed, look, title, role, status, responsibilities, builtin, spent_month_micros }; | |
| 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. | 18 | } |
| 19 | ||
| 20 | /** Where a person's profile is. */ | |
| 21 | export function personPath(workspace: string, username: string): string { | |
| 22 | return `/${workspace}/-/people/${username}`; | |
| 23 | } | |
| 24 | ||
| People lists people. The directory, its counts and its search cover the workspace's people only; agents keep their page under Agents, where the Profile tab now also says what each agent is told about its teams, and the old address of an agent under People sends you there. Agents still join teams beside people: a team's members page groups them under its people with an agent badge, and the org chart shows them as members of the team beside its lead. The people and teams, agents, teams and workspaces guides say so. | 25 | /** Where an agent's profile is: its page in Agents. People has none; its old address there redirects. */ |
| 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. | 26 | export function agentPath(workspace: string, handle: string): string { |
| People lists people. The directory, its counts and its search cover the workspace's people only; agents keep their page under Agents, where the Profile tab now also says what each agent is told about its teams, and the old address of an agent under People sends you there. Agents still join teams beside people: a team's members page groups them under its people with an agent badge, and the org chart shows them as members of the team beside its lead. The people and teams, agents, teams and workspaces guides say so. | 27 | return `/${workspace}/-/agents/${handle}`; |
| 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. | 28 | } |
| 29 | ||
| 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. | 30 | /** The agents on a team, in the order they were added, as found among `agents`. */ |
| 31 | export function agentsOn<A extends Pick<PeopleAgent, "id">>(team: Pick<DirectoryTeam, "agent_ids">, agents: readonly A[]): A[] { | |
| 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. | 32 | const byId = new Map(agents.map((agent) => [agent.id, agent])); |
| 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. | 33 | return [...new Set(team.agent_ids)].map((id) => byId.get(id)).filter((agent): agent is A => !!agent); |
| 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. | 34 | } |
| 35 | ||
| 36 | /** The teams a person is on. */ | |
| 37 | export function teamsOfPerson<T extends Pick<DirectoryTeam, "people">>(teams: readonly T[], username: string): T[] { | |
| 38 | return teams.filter((team) => team.people.some((person) => person.username === username)); | |
| 39 | } | |
| 40 | ||
| 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. | 41 | /** The teams an agent is on: its memberships, as a person's are. */ |
| 42 | export function teamsOfAgent<T extends Pick<DirectoryTeam, "agent_ids">>(teams: readonly T[], agent: Pick<PeopleAgent, "id">): T[] { | |
| 43 | return teams.filter((team) => team.agent_ids.includes(agent.id)); | |
| 44 | } | |
| 45 | ||
| 46 | /** A team an agent is on, as the Agents sidebar groups by it. */ | |
| 47 | export type AgentTeamRef = { slug: string; name: string }; | |
| 48 | ||
| 49 | /** Each agent's teams, by agent id, from teams with the agents on them; the teams by name. */ | |
| 50 | export function teamsByAgent(teams: readonly { slug: string; name: string; agent_ids: readonly string[] }[]): Record<string, AgentTeamRef[]> { | |
| 51 | const out: Record<string, AgentTeamRef[]> = {}; | |
| 52 | for (const team of [...teams].sort((a, b) => a.name.localeCompare(b.name))) { | |
| 53 | for (const id of new Set(team.agent_ids)) (out[id] ??= []).push({ slug: team.slug, name: team.name }); | |
| 54 | } | |
| 55 | return 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. | 56 | } |
| 57 | ||
| 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. | 58 | /** The label for agents on no team, wherever agents are grouped by team. */ |
| 59 | export const NO_TEAM = "Not on a team"; | |
| 60 | ||
| 61 | /** | |
| 62 | * Agents grouped by the teams they are on, as the Agents sidebar shows | |
| 63 | * them: each team by name, an agent on two teams under both, and agents on | |
| 64 | * none last, under "Not on a team". Each group's agents by name. | |
| 65 | */ | |
| 66 | export function groupByTeam<A extends { id: string; display_name: string }>(agents: readonly A[], teamsOf: Record<string, readonly AgentTeamRef[]>): { key: string; label: string; agents: A[] }[] { | |
| 67 | const groups = new Map<string, { key: string; label: string; agents: A[] }>(); | |
| 68 | const none: A[] = []; | |
| 69 | for (const agent of agents) { | |
| 70 | const teams = teamsOf[agent.id] ?? []; | |
| 71 | if (!teams.length) none.push(agent); | |
| 72 | for (const team of teams) { | |
| 73 | const group = groups.get(team.slug) ?? { key: team.slug, label: team.name, agents: [] }; | |
| 74 | if (!group.agents.includes(agent)) group.agents.push(agent); | |
| 75 | groups.set(team.slug, group); | |
| 76 | } | |
| 77 | } | |
| 78 | const byName = (a: A, b: A) => a.display_name.localeCompare(b.display_name); | |
| 79 | const out = [...groups.values()].sort((a, b) => a.label.localeCompare(b.label)).map((group) => ({ ...group, agents: group.agents.sort(byName) })); | |
| 80 | if (none.length) out.push({ key: "", label: NO_TEAM, agents: none.sort(byName) }); | |
| 81 | return out; | |
| 82 | } | |
| 83 | ||
| 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. | 84 | /** Whether `lead` is this person or this agent. */ |
| 85 | export function leads(lead: TeamLead | null, who: { username: string } | { id: string }): boolean { | |
| 86 | if (!lead) return false; | |
| 87 | if ("username" in who) return lead.kind === "user" && lead.username === who.username; | |
| 88 | return lead.kind === "agent" && lead.agent_id === who.id; | |
| 89 | } | |
| 90 | ||
| 91 | /** A person's name as pages show it. */ | |
| 92 | export function personName(person: Pick<DirectoryPerson, "username" | "name" | "display_username">): string { | |
| 93 | return person.name?.trim() || person.display_username || person.username; | |
| 94 | } | |
| 95 | ||
| 96 | /** Whether every word of `query` is somewhere in `fields`. */ | |
| 97 | export function matches(query: string, fields: readonly (string | null | undefined)[]): boolean { | |
| 98 | const words = query.toLowerCase().split(/\s+/).filter(Boolean); | |
| 99 | if (!words.length) return true; | |
| 100 | const text = fields.filter(Boolean).join(" ").toLowerCase(); | |
| 101 | return words.every((word) => text.includes(word)); | |
| 102 | } | |
| 103 | ||
| People lists people. The directory, its counts and its search cover the workspace's people only; agents keep their page under Agents, where the Profile tab now also says what each agent is told about its teams, and the old address of an agent under People sends you there. Agents still join teams beside people: a team's members page groups them under its people with an agent badge, and the org chart shows them as members of the team beside its lead. The people and teams, agents, teams and workspaces guides say so. | 104 | /** One card in the directory: a person and their teams. */ |
| 105 | export type DirectoryEntry = { person: DirectoryPerson; teams: DirectoryTeam[] }; | |
| 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. | 106 | |
| 107 | /** | |
| People lists people. The directory, its counts and its search cover the workspace's people only; agents keep their page under Agents, where the Profile tab now also says what each agent is told about its teams, and the old address of an agent under People sends you there. Agents still join teams beside people: a team's members page groups them under its people with an agent badge, and the org chart shows them as members of the team beside its lead. The people and teams, agents, teams and workspaces guides say so. | 108 | * The people matching `query`, in the directory's order (by name). A |
| 109 | * person matches on their name, username, title, teams, what they own, | |
| 110 | * where they are and their bio. The directory is people only: the | |
| 111 | * workspace's agents are listed under Agents and on the teams they are | |
| 112 | * on, never here, so a query that would only match an agent finds no 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. | 113 | */ |
| People lists people. The directory, its counts and its search cover the workspace's people only; agents keep their page under Agents, where the Profile tab now also says what each agent is told about its teams, and the old address of an agent under People sends you there. Agents still join teams beside people: a team's members page groups them under its people with an agent badge, and the org chart shows them as members of the team beside its lead. The people and teams, agents, teams and workspaces guides say so. | 114 | export function directoryEntries(directory: PeopleDirectory, query: string): DirectoryEntry[] { |
| 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. | 115 | const out: DirectoryEntry[] = []; |
| People lists people. The directory, its counts and its search cover the workspace's people only; agents keep their page under Agents, where the Profile tab now also says what each agent is told about its teams, and the old address of an agent under People sends you there. Agents still join teams beside people: a team's members page groups them under its people with an agent badge, and the org chart shows them as members of the team beside its lead. The people and teams, agents, teams and workspaces guides say so. | 116 | for (const person of directory.people) { |
| 117 | const teams = teamsOfPerson(directory.teams, person.username); | |
| 118 | const fields = [person.username, person.name, person.title, person.location, person.bio, ...person.owns, ...teams.map((team) => team.name), person.role === "owner" ? "owner" : null]; | |
| 119 | if (matches(query, fields)) out.push({ person, teams }); | |
| 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. | 120 | } |
| 121 | return out; | |
| 122 | } | |
| 123 | ||
| 124 | /** One person in the org chart, with who reports to them and the agents of the teams they lead. */ | |
| 125 | export type OrgNode = { person: DirectoryPerson; agents: PeopleAgent[]; leads: DirectoryTeam[]; reports: OrgNode[] }; | |
| 126 | ||
| 127 | export type OrgChart = { | |
| 128 | /** Everyone at the top: no manager, or one who is no longer here. */ | |
| 129 | roots: OrgNode[]; | |
| 130 | /** Teams no person leads (an agent does, or no one), with their agents. */ | |
| 131 | unled: { team: DirectoryTeam; agents: PeopleAgent[] }[]; | |
| 132 | }; | |
| 133 | ||
| 134 | /** | |
| 135 | * Reporting lines, with each team's agents beside the person who leads it. | |
| 136 | * A loop (which identity refuses) would leave its people out of `roots`; | |
| 137 | * they are put back at the top, so no one is ever missing. | |
| 138 | */ | |
| 139 | export function orgChart(directory: PeopleDirectory, agents: readonly PeopleAgent[]): OrgChart { | |
| 140 | const known = new Set(directory.people.map((person) => person.username)); | |
| 141 | const byManager = new Map<string, DirectoryPerson[]>(); | |
| 142 | for (const person of directory.people) { | |
| 143 | if (person.manager && known.has(person.manager) && person.manager !== person.username) { | |
| 144 | byManager.set(person.manager, [...(byManager.get(person.manager) ?? []), person]); | |
| 145 | } | |
| 146 | } | |
| 147 | const placed = new Set<string>(); | |
| 148 | const node = (person: DirectoryPerson): OrgNode => { | |
| 149 | placed.add(person.username); | |
| 150 | const led = directory.teams.filter((team) => leads(team.lead, { username: person.username })); | |
| 151 | const theirAgents: PeopleAgent[] = []; | |
| 152 | for (const team of led) for (const agent of agentsOn(team, agents)) if (!theirAgents.includes(agent)) theirAgents.push(agent); | |
| 153 | const reports = (byManager.get(person.username) ?? []).filter((report) => !placed.has(report.username)).map(node); | |
| 154 | return { person, agents: theirAgents, leads: led, reports }; | |
| 155 | }; | |
| 156 | const roots = directory.people.filter((person) => !person.manager || !known.has(person.manager)).map(node); | |
| 157 | for (const person of directory.people) if (!placed.has(person.username)) roots.push(node(person)); | |
| 158 | const unled = directory.teams | |
| 159 | .filter((team) => !team.lead || team.lead.kind === "agent") | |
| 160 | .map((team) => ({ team, agents: agentsOn(team, agents) })) | |
| 161 | .filter((entry) => entry.agents.length > 0); | |
| 162 | return { roots, unled }; | |
| 163 | } | |
| 164 | ||
| 165 | /** Everyone who reports to `username`, directly. */ | |
| 166 | export function reportsOf(people: readonly DirectoryPerson[], username: string): DirectoryPerson[] { | |
| 167 | return people.filter((person) => person.manager === username && person.username !== username); | |
| 168 | } | |
| 169 | ||
| 170 | /** Who `username` could report to: anyone but themselves and the people under them. */ | |
| 171 | export function managerChoices(people: readonly DirectoryPerson[], username: string): DirectoryPerson[] { | |
| 172 | const below = new Set<string>([username]); | |
| 173 | let grew = true; | |
| 174 | while (grew) { | |
| 175 | grew = false; | |
| 176 | for (const person of people) { | |
| 177 | if (person.manager && below.has(person.manager) && !below.has(person.username)) { | |
| 178 | below.add(person.username); | |
| 179 | grew = true; | |
| 180 | } | |
| 181 | } | |
| 182 | } | |
| 183 | return people.filter((person) => !below.has(person.username)); | |
| 184 | } | |
| 185 | ||
| 186 | /** What someone owns, from a form's text: one per line or comma. */ | |
| 187 | export function ownsFromText(text: string): string[] { | |
| 188 | return text | |
| 189 | .split(/[\n,]+/) | |
| 190 | .map((phrase) => phrase.trim()) | |
| 191 | .filter(Boolean); | |
| 192 | } | |
| 193 | ||
| 194 | /** A monthly budget in dollars from a form, as micro-dollars; 0 for none; null when it isn't a number. */ | |
| 195 | export function budgetFromText(text: string): number | null { | |
| 196 | const value = text.trim().replace(/^\$/, "").replace(/,/g, ""); | |
| 197 | if (!value) return 0; | |
| 198 | const dollars = Number(value); | |
| 199 | if (!Number.isFinite(dollars) || dollars < 0) return null; | |
| 200 | return Math.round(dollars * 1_000_000); | |
| 201 | } | |
| 202 | ||
| 203 | /** The access to Code a member has, in words: their role, the base permission, and the teams that add to it. */ | |
| 204 | export function codeAccessWords( | |
| 205 | person: Pick<DirectoryPerson, "role">, | |
| 206 | base: PeopleDirectory["base_permission"], | |
| 207 | teams: readonly Pick<DirectoryTeam, "name" | "repos_count">[], | |
| 208 | ): { summary: string; through: string[] } { | |
| 209 | const summary = | |
| 210 | person.role === "owner" | |
| 211 | ? "Admin on every repository, as an owner" | |
| 212 | : base === "none" | |
| 213 | ? "Only repositories they're given a role on" | |
| 214 | : `${base[0]!.toUpperCase()}${base.slice(1)} on every repository, the workspace's base permission`; | |
| 215 | const through = teams | |
| 216 | .filter((team) => team.repos_count > 0) | |
| 217 | .map((team) => `Roles on ${team.repos_count} ${team.repos_count === 1 ? "repository" : "repositories"} through ${team.name}`); | |
| 218 | return { summary, through }; | |
| 219 | } | |
| 220 | ||
| 221 | /** The part of what an agent is told that is about one team: its heading and what follows, up to the next. */ | |
| 222 | export function teamBlock(text: string | null, teamName: string): string | null { | |
| 223 | if (!text) return null; | |
| 224 | const blocks = text.split(/\n(?=### )/); | |
| 225 | const found = blocks.find((block) => block.startsWith(`### ${teamName}\n`) || block.trim() === `### ${teamName}`); | |
| 226 | return found ? found.trim() : null; | |
| 227 | } | |
| 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. | 228 | |
| 229 | /** A team an agent is on, as its profile lists it. */ | |
| 230 | export type AgentTeamRow = { | |
| 231 | slug: string; | |
| 232 | name: string; | |
| 233 | people: number; | |
| 234 | agents: number; | |
| 235 | lead: boolean; | |
| 236 | /** Whether the viewer may take it off (owners and the team's maintainers). */ | |
| 237 | can_manage: boolean; | |
| 238 | }; | |
| 239 | ||
| 240 | export type AgentTeams = { | |
| 241 | on: AgentTeamRow[]; | |
| 242 | /** Teams it isn't on that the viewer manages, to add it to. */ | |
| 243 | addable: { slug: string; name: string }[]; | |
| 244 | }; | |
| 245 | ||
| 246 | /** What adding an agent to a team, or taking it off, answers. */ | |
| 247 | export type TeamChange = { intent: "join-team" | "leave-team"; team: string; error: string | null }; |