Skip to content
247 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/**
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 */
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,
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. */
15export 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. */
21export 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.26export 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`. */
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
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. */
105export 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.114export 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. */
125export type OrgNode = { person: DirectoryPerson; agents: PeopleAgent[]; leads: DirectoryTeam[]; reports: OrgNode[] };
126
127export 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 */
139export 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. */
166export 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. */
171export 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. */
187export 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. */
195export 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. */
204export 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. */
222export 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. */
230export 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
240export 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. */
247export type TeamChange = { intent: "join-team" | "leave-team"; team: string; error: string | null };