Skip to content
188 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.1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { DirectoryPerson, DirectoryTeam, PeopleDirectory } from "@g1t/contracts";
5
6import {
7 type 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.8 NO_TEAM,
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.9 agentPath,
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.10 agentsOn,
11 groupByTeam,
12 teamsByAgent,
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.13 budgetFromText,
14 codeAccessWords,
15 directoryEntries,
16 managerChoices,
17 orgChart,
18 ownsFromText,
19 teamBlock,
20 teamsOfAgent,
21} from "./people.ts";
22
23const person = (username: string, extra: Partial<DirectoryPerson> = {}): DirectoryPerson => ({
24 user_id: `usr_${username}`,
25 username,
26 name: null,
27 avatar: null,
28 bio: null,
29 location: null,
30 pronouns: null,
31 timezone: null,
32 role: "member",
33 title: null,
34 manager: null,
35 owns: [],
36 joined_at: "2026-10-01T00:00:00Z",
37 ...extra,
38});
39
40const team = (slug: string, name: string, extra: Partial<DirectoryTeam> = {}): DirectoryTeam => ({
41 slug,
42 name,
43 description: null,
44 visibility: "visible",
45 parent: null,
46 lead: null,
47 channel: null,
48 budget_micros: null,
49 people: [],
50 agent_ids: [],
51 repos_count: 0,
52 ...extra,
53});
54
55const agent = (id: string, handle: string, extra: Partial<PeopleAgent> = {}): PeopleAgent => ({
56 id,
57 handle,
58 display_name: handle[0]!.toUpperCase() + handle.slice(1),
59 avatar: null,
60 avatar_seed: handle,
61 title: "",
62 role: "",
63 status: "idle",
64 responsibilities: [],
65 builtin: false,
66 spent_month_micros: 0,
67 ...extra,
68});
69
70const directory: PeopleDirectory = {
71 people: [
72 person("chase", { name: "Chase Pierce", title: "Founder", role: "owner" }),
73 person("sofia", { name: "Sofia Reyes", title: "Head of Sales", manager: "chase", owns: ["the forecast"] }),
74 person("jordan", { name: "Jordan Lee", title: "Account Executive", manager: "sofia", owns: ["Halcyon", "Bluebird"] }),
75 person("kai", { name: "Kai Nakamura", title: "Product Designer", manager: "chase" }),
76 ],
77 teams: [
78 team("sales", "Sales", {
79 lead: { kind: "user", username: "sofia", name: "Sofia Reyes", avatar: null },
80 people: [
81 { username: "sofia", role: "maintainer" },
82 { username: "jordan", role: "member" },
83 ],
84 agent_ids: ["agt_david"],
85 repos_count: 0,
86 }),
87 team("design", "Design", { people: [{ username: "kai", role: "maintainer" }], repos_count: 2 }),
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.88 team("night-shift", "Night shift", { lead: { kind: "agent", agent_id: "agt_atlas" }, agent_ids: ["agt_atlas", "agt_sentinel", "agt_david"] }),
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.89 ],
90 base_permission: "read",
91 can_manage: true,
92};
93
94const agents = [
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.95 agent("agt_david", "david", { title: "CRM keeper" }),
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.96 agent("agt_atlas", "atlas", { title: "Night watch" }),
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.97 agent("agt_sentinel", "sentinel", { title: "Error watch", responsibilities: ["Watches errors"] }),
98 agent("agt_pax", "pax", { title: "Billing" }),
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.99];
100
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.101test("an agent is on the teams it is a member of, like anyone", () => {
102 assert.deepEqual(agentsOn(directory.teams[2]!, agents).map((a) => a.id), ["agt_atlas", "agt_sentinel", "agt_david"]);
103 assert.deepEqual(teamsOfAgent(directory.teams, agents[0]!).map((t) => t.slug), ["sales", "night-shift"]);
104 assert.deepEqual(teamsOfAgent(directory.teams, agents[3]!), []);
105});
106
107test("the Agents sidebar groups agents by their teams: under each team, and the rest last", () => {
108 const teams = teamsByAgent(directory.teams);
109 assert.deepEqual(teams.agt_david, [{ slug: "night-shift", name: "Night shift" }, { slug: "sales", name: "Sales" }]);
110 const groups = groupByTeam(agents, teams).map((g) => [g.label, g.agents.map((a) => a.handle)]);
111 assert.deepEqual(groups, [
112 ["Night shift", ["atlas", "david", "sentinel"]],
113 ["Sales", ["david"]],
114 [NO_TEAM, ["pax"]],
115 ]);
116 assert.deepEqual(groupByTeam(agents.slice(3), {}).map((g) => g.label), [NO_TEAM]);
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.117});
118
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.119test("the directory lists people only, in one search; agents are never in it", () => {
120 const names = (query: string) => directoryEntries(directory, query).map((e) => e.person.username);
121 assert.deepEqual(names(""), ["chase", "sofia", "jordan", "kai"]);
122 assert.deepEqual(names("sales"), ["sofia", "jordan"]);
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.123 assert.deepEqual(names("halcyon"), ["jordan"]);
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.124 assert.deepEqual(names("owner"), ["chase"]);
125 // What only an agent would match (its handle, title or responsibilities) finds no one here.
126 assert.deepEqual(names("david"), []);
127 assert.deepEqual(names("errors"), []);
128 // A team of agents only is no one's team in the directory.
129 assert.deepEqual(names("night shift"), []);
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.130 assert.equal(names("nobody here").length, 0);
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.131 // Every entry is a person with their teams; nothing in the shape says "agent".
132 const sofia = directoryEntries(directory, "sofia")[0]!;
133 assert.deepEqual(Object.keys(sofia), ["person", "teams"]);
134 assert.deepEqual(sofia.teams.map((t) => t.slug), ["sales"]);
135});
136
137test("an agent's profile is its page in Agents, where People links to it", () => {
138 assert.equal(agentPath("acme", "margo"), "/acme/-/agents/margo");
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.139});
140
141test("the org chart follows reporting lines, with each team's agents beside its lead", () => {
142 const chart = orgChart(directory, agents);
143 assert.deepEqual(chart.roots.map((n) => n.person.username), ["chase"]);
144 const chase = chart.roots[0]!;
145 assert.deepEqual(chase.reports.map((n) => n.person.username), ["sofia", "kai"]);
146 const sofia = chase.reports[0]!;
147 assert.deepEqual(sofia.agents.map((a) => a.handle), ["david"]);
148 assert.deepEqual(sofia.reports.map((n) => n.person.username), ["jordan"]);
149 // A team an agent leads sits apart, with its agents.
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.150 assert.deepEqual(chart.unled.map((u) => [u.team.slug, u.agents.map((a) => a.handle)]), [["night-shift", ["atlas", "sentinel", "david"]]]);
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.151});
152
153test("no one goes missing from the org chart, even in a loop", () => {
154 const looped: PeopleDirectory = { ...directory, people: [person("a", { manager: "b" }), person("b", { manager: "a" })] };
155 const chart = orgChart(looped, []);
156 const all = (nodes: typeof chart.roots): string[] => nodes.flatMap((n) => [n.person.username, ...all(n.reports)]);
157 assert.deepEqual(all(chart.roots).sort(), ["a", "b"]);
158});
159
160test("someone can report to anyone but themselves and the people under them", () => {
161 assert.deepEqual(managerChoices(directory.people, "sofia").map((p) => p.username), ["chase", "kai"]);
162 assert.deepEqual(managerChoices(directory.people, "jordan").map((p) => p.username), ["chase", "sofia", "kai"]);
163});
164
165test("forms read plainly", () => {
166 assert.deepEqual(ownsFromText("storefront\n the release process ,, billing"), ["storefront", "the release process", "billing"]);
167 assert.equal(budgetFromText(""), 0);
168 assert.equal(budgetFromText("$150"), 150_000_000);
169 assert.equal(budgetFromText("1,250.50"), 1_250_500_000);
170 assert.equal(budgetFromText("lots"), null);
171 assert.equal(budgetFromText("-5"), null);
172});
173
174test("access to Code says where it comes from", () => {
175 assert.equal(codeAccessWords({ role: "owner" }, "read", []).summary, "Admin on every repository, as an owner");
176 const kai = codeAccessWords({ role: "member" }, "read", [directory.teams[1]!]);
177 assert.equal(kai.summary, "Read on every repository, the workspace's base permission");
178 assert.deepEqual(kai.through, ["Roles on 2 repositories through Design"]);
179 assert.equal(codeAccessWords({ role: "member" }, "none", []).summary, "Only repositories they're given a role on");
180});
181
182test("a team's part of what an agent is told", () => {
183 const told = "## Your teams\n\nIntro.\n\n### Sales\n\nLed by Sofia.\n- @sofia\n\n### Support\n\n- @dev";
184 assert.equal(teamBlock(told, "Sales"), "### Sales\n\nLed by Sofia.\n- @sofia");
185 assert.equal(teamBlock(told, "Support"), "### Support\n\n- @dev");
186 assert.equal(teamBlock(told, "Design"), null);
187 assert.equal(teamBlock(null, "Sales"), null);
188});