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