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 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import type { DirectoryPerson, DirectoryTeam, PeopleDirectory } from "@g1t/contracts"; | |
| 5 | ||
| 6 | import { | |
| 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 | ||
| 23 | const 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 | ||
| 40 | const 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 | ||
| 55 | const 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 | ||
| 70 | const 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 | ||
| 94 | const 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. | 101 | test("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 | ||
| 107 | test("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. | 119 | test("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 | ||
| 137 | test("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 | ||
| 141 | test("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 | ||
| 153 | test("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 | ||
| 160 | test("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 | ||
| 165 | test("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 | ||
| 174 | test("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 | ||
| 182 | test("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 | }); |