Skip to content
263 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.

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/**
2 * People on the site: the directory (people and agents in one search),
3 * profiles, teams of any mix, and the org chart. Types only from the
4 * contracts, so this file runs under `node --test`.
5 */
6import type { DirectoryPerson, DirectoryTeam, PeopleDirectory, TeamLead, WorkspaceAgent } from "@g1t/contracts";
7
8/** An agent as People's pages need it. */
9export type PeopleAgent = Pick<
10 WorkspaceAgent,
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.11 "id" | "handle" | "display_name" | "avatar" | "avatar_seed" | "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. */
15export function peopleAgent(agent: PeopleAgent): PeopleAgent {
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.16 const { id, handle, display_name, avatar, avatar_seed, title, role, status, responsibilities, builtin, spent_month_micros } = agent;
17 return { id, handle, display_name, avatar, avatar_seed, 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. */
21export function personPath(workspace: string, username: string): string {
22 return `/${workspace}/-/people/${username}`;
23}
24
25/** Where an agent's People profile is. */
26export function agentPath(workspace: string, handle: string): string {
27 return `/${workspace}/-/people/agents/${handle}`;
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`. */
31export 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. */
37export 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. */
42export 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. */
47export 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. */
50export 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. */
59export 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 */
66export 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. */
85export 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. */
92export 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`. */
97export 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
104export type DirectoryKind = "everyone" | "people" | "agents";
105
106export function directoryKind(value: string | null): DirectoryKind {
107 return value === "people" || value === "agents" ? value : "everyone";
108}
109
110/** One card in the directory: a person or an agent. */
111export type DirectoryEntry =
112 | { kind: "person"; person: DirectoryPerson; teams: DirectoryTeam[] }
113 | { kind: "agent"; agent: PeopleAgent; teams: DirectoryTeam[] };
114
115/**
116 * Everyone matching `query`, people first by name, then agents (the
117 * orchestrator first, then by name). People match on their name, title,
118 * teams, what they own, where they are and their bio; agents on their
119 * name, handle, title, role, teams and responsibilities.
120 */
121export function directoryEntries(directory: PeopleDirectory, agents: readonly PeopleAgent[], query: string, kind: DirectoryKind): DirectoryEntry[] {
122 const out: DirectoryEntry[] = [];
123 if (kind !== "agents") {
124 for (const person of directory.people) {
125 const teams = teamsOfPerson(directory.teams, person.username);
126 const fields = [person.username, person.name, person.title, person.location, person.bio, ...person.owns, ...teams.map((team) => team.name), person.role === "owner" ? "owner" : null];
127 if (matches(query, fields)) out.push({ kind: "person", person, teams });
128 }
129 }
130 if (kind !== "people") {
131 for (const agent of agents) {
132 const teams = teamsOfAgent(directory.teams, 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.133 const fields = [agent.handle, agent.display_name, agent.title, agent.role, ...agent.responsibilities, ...teams.map((team) => team.name), "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.134 if (matches(query, fields)) out.push({ kind: "agent", agent, teams });
135 }
136 }
137 return out;
138}
139
140/** One person in the org chart, with who reports to them and the agents of the teams they lead. */
141export type OrgNode = { person: DirectoryPerson; agents: PeopleAgent[]; leads: DirectoryTeam[]; reports: OrgNode[] };
142
143export type OrgChart = {
144 /** Everyone at the top: no manager, or one who is no longer here. */
145 roots: OrgNode[];
146 /** Teams no person leads (an agent does, or no one), with their agents. */
147 unled: { team: DirectoryTeam; agents: PeopleAgent[] }[];
148};
149
150/**
151 * Reporting lines, with each team's agents beside the person who leads it.
152 * A loop (which identity refuses) would leave its people out of `roots`;
153 * they are put back at the top, so no one is ever missing.
154 */
155export function orgChart(directory: PeopleDirectory, agents: readonly PeopleAgent[]): OrgChart {
156 const known = new Set(directory.people.map((person) => person.username));
157 const byManager = new Map<string, DirectoryPerson[]>();
158 for (const person of directory.people) {
159 if (person.manager && known.has(person.manager) && person.manager !== person.username) {
160 byManager.set(person.manager, [...(byManager.get(person.manager) ?? []), person]);
161 }
162 }
163 const placed = new Set<string>();
164 const node = (person: DirectoryPerson): OrgNode => {
165 placed.add(person.username);
166 const led = directory.teams.filter((team) => leads(team.lead, { username: person.username }));
167 const theirAgents: PeopleAgent[] = [];
168 for (const team of led) for (const agent of agentsOn(team, agents)) if (!theirAgents.includes(agent)) theirAgents.push(agent);
169 const reports = (byManager.get(person.username) ?? []).filter((report) => !placed.has(report.username)).map(node);
170 return { person, agents: theirAgents, leads: led, reports };
171 };
172 const roots = directory.people.filter((person) => !person.manager || !known.has(person.manager)).map(node);
173 for (const person of directory.people) if (!placed.has(person.username)) roots.push(node(person));
174 const unled = directory.teams
175 .filter((team) => !team.lead || team.lead.kind === "agent")
176 .map((team) => ({ team, agents: agentsOn(team, agents) }))
177 .filter((entry) => entry.agents.length > 0);
178 return { roots, unled };
179}
180
181/** Everyone who reports to `username`, directly. */
182export function reportsOf(people: readonly DirectoryPerson[], username: string): DirectoryPerson[] {
183 return people.filter((person) => person.manager === username && person.username !== username);
184}
185
186/** Who `username` could report to: anyone but themselves and the people under them. */
187export function managerChoices(people: readonly DirectoryPerson[], username: string): DirectoryPerson[] {
188 const below = new Set<string>([username]);
189 let grew = true;
190 while (grew) {
191 grew = false;
192 for (const person of people) {
193 if (person.manager && below.has(person.manager) && !below.has(person.username)) {
194 below.add(person.username);
195 grew = true;
196 }
197 }
198 }
199 return people.filter((person) => !below.has(person.username));
200}
201
202/** What someone owns, from a form's text: one per line or comma. */
203export function ownsFromText(text: string): string[] {
204 return text
205 .split(/[\n,]+/)
206 .map((phrase) => phrase.trim())
207 .filter(Boolean);
208}
209
210/** A monthly budget in dollars from a form, as micro-dollars; 0 for none; null when it isn't a number. */
211export function budgetFromText(text: string): number | null {
212 const value = text.trim().replace(/^\$/, "").replace(/,/g, "");
213 if (!value) return 0;
214 const dollars = Number(value);
215 if (!Number.isFinite(dollars) || dollars < 0) return null;
216 return Math.round(dollars * 1_000_000);
217}
218
219/** The access to Code a member has, in words: their role, the base permission, and the teams that add to it. */
220export function codeAccessWords(
221 person: Pick<DirectoryPerson, "role">,
222 base: PeopleDirectory["base_permission"],
223 teams: readonly Pick<DirectoryTeam, "name" | "repos_count">[],
224): { summary: string; through: string[] } {
225 const summary =
226 person.role === "owner"
227 ? "Admin on every repository, as an owner"
228 : base === "none"
229 ? "Only repositories they're given a role on"
230 : `${base[0]!.toUpperCase()}${base.slice(1)} on every repository, the workspace's base permission`;
231 const through = teams
232 .filter((team) => team.repos_count > 0)
233 .map((team) => `Roles on ${team.repos_count} ${team.repos_count === 1 ? "repository" : "repositories"} through ${team.name}`);
234 return { summary, through };
235}
236
237/** The part of what an agent is told that is about one team: its heading and what follows, up to the next. */
238export function teamBlock(text: string | null, teamName: string): string | null {
239 if (!text) return null;
240 const blocks = text.split(/\n(?=### )/);
241 const found = blocks.find((block) => block.startsWith(`### ${teamName}\n`) || block.trim() === `### ${teamName}`);
242 return found ? found.trim() : null;
243}
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.244
245/** A team an agent is on, as its profile lists it. */
246export type AgentTeamRow = {
247 slug: string;
248 name: string;
249 people: number;
250 agents: number;
251 lead: boolean;
252 /** Whether the viewer may take it off (owners and the team's maintainers). */
253 can_manage: boolean;
254};
255
256export type AgentTeams = {
257 on: AgentTeamRow[];
258 /** Teams it isn't on that the viewer manages, to add it to. */
259 addable: { slug: string; name: string }[];
260};
261
262/** What adding an agent to a team, or taking it off, answers. */
263export type TeamChange = { intent: "join-team" | "leave-team"; team: string; error: string | null };