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.
| Chat and workspace agents: channels, DMs and named agents you talk to | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import type { User } from "@g1t/contracts"; | |
| 5 | ||
| 6 | import { canManage, canSee } from "./access.ts"; | |
| 7 | import { DEFAULT_AUTONOMY, DEFAULT_CAPACITY, applyChanges, readJson } from "./definition.ts"; | |
| 8 | import { TEMPLATES, TEMPLATE_IDS } from "./templates.ts"; | |
| 9 | ||
| 10 | const input = { handle: "Ship", display_name: " Ship ", role: "Release manager", instructions: "Cut releases." }; | |
| 11 | ||
| 12 | test("a new agent gets safe defaults", () => { | |
| 13 | const made = applyChanges(null, input, TEMPLATE_IDS); | |
| 14 | assert.ok(made.ok); | |
| 15 | if (!made.ok) return; | |
| 16 | assert.equal(made.value.handle, "ship"); | |
| 17 | assert.equal(made.value.display_name, "Ship"); | |
| 18 | assert.equal(made.value.personality_preset, "crisp"); | |
| 19 | assert.equal(made.value.capacity, DEFAULT_CAPACITY); | |
| 20 | assert.deepEqual(made.value.autonomy, DEFAULT_AUTONOMY); | |
| 21 | assert.equal(made.value.autonomy.merge, "approval"); | |
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 22 | assert.deepEqual(made.value.routing, { floor: null, ceiling: null, providers: [], pinned: null, effort: "auto" }); |
| Chat and workspace agents: channels, DMs and named agents you talk to | 23 | }); |
| 24 | ||
| 25 | test("a new agent needs a handle, a name, a role and instructions", () => { | |
| 26 | for (const missing of ["handle", "display_name", "role", "instructions"] as const) { | |
| 27 | const made = applyChanges(null, { ...input, [missing]: "" }, TEMPLATE_IDS); | |
| 28 | assert.equal(made.ok, false, missing); | |
| 29 | } | |
| 30 | assert.equal(applyChanges(null, { ...input, handle: "g1t" }, TEMPLATE_IDS).ok, false); | |
| 31 | }); | |
| 32 | ||
| 33 | test("a change merges onto the definition, and is checked", () => { | |
| 34 | const made = applyChanges(null, input, TEMPLATE_IDS); | |
| 35 | assert.ok(made.ok); | |
| 36 | if (!made.ok) return; | |
| 37 | const changed = applyChanges(made.value, { routing: { floor: "large" }, budget: { monthly_micros: 5_000_000 }, autonomy: { merge: "never" } }, TEMPLATE_IDS); | |
| 38 | assert.ok(changed.ok); | |
| 39 | if (!changed.ok) return; | |
| 40 | assert.equal(changed.value.handle, "ship", "untouched fields stay"); | |
| 41 | assert.equal(changed.value.routing.floor, "large"); | |
| 42 | assert.equal(changed.value.routing.ceiling, null); | |
| 43 | assert.equal(changed.value.budget.monthly_micros, 5_000_000); | |
| 44 | assert.equal(changed.value.budget.daily_micros, null); | |
| 45 | assert.equal(changed.value.autonomy.merge, "never"); | |
| 46 | assert.equal(changed.value.autonomy.open_pull_requests, "alone"); | |
| 47 | ||
| 48 | const bad = (changes: object) => applyChanges(made.value, changes, TEMPLATE_IDS).ok; | |
| 49 | assert.equal(bad({ routing: { floor: "frontier", ceiling: "small" } }), false, "floor above ceiling"); | |
| 50 | assert.equal(bad({ routing: { floor: "huge" } }), false); | |
| 51 | assert.equal(bad({ routing: { pinned: "no-slash" } }), false); | |
| 52 | assert.equal(bad({ budget: { monthly_micros: -1 } }), false); | |
| 53 | assert.equal(bad({ budget: { daily_micros: 1.5 } }), false); | |
| 54 | assert.equal(bad({ autonomy: { deploy_production: "alone" } }), false, "production deploys always ask"); | |
| 55 | assert.equal(bad({ capacity: 0 }), false); | |
| 56 | assert.equal(bad({ capacity: 11 }), false); | |
| 57 | assert.equal(bad({ personality_preset: "pirate" }), false); | |
| 58 | assert.equal(bad({ template: "nope" }), false); | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 59 | assert.equal(bad({ template: "qa" }), true); |
| Chat and workspace agents: channels, DMs and named agents you talk to | 60 | for (const providers of [[], ["g1t"], ["workspace"], ["int_1"], ["g1t", "workspace"]]) assert.equal(bad({ routing: { providers } }), true, JSON.stringify(providers)); |
| 61 | assert.equal(bad({ routing: { providers: [""] } }), false); | |
| 62 | assert.equal(bad({ instructions: "x".repeat(8001) }), false); | |
| 63 | }); | |
| 64 | ||
| 65 | test("a stored JSON column that cannot be read is the default", () => { | |
| 66 | assert.deepEqual(readJson("{bad", { a: 1 }), { a: 1 }); | |
| 67 | assert.deepEqual(readJson('{"b":2}', { a: 1 }), { a: 1, b: 2 }); | |
| 68 | assert.deepEqual(readJson(null, { a: 1 }), { a: 1 }); | |
| 69 | }); | |
| 70 | ||
| 71 | test("every template is a valid definition with the routing its job needs", () => { | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 72 | assert.deepEqual(TEMPLATE_IDS, ["engineering", "qa", "operations", "docs", "product", "support", "sales"]); |
| Chat and workspace agents: channels, DMs and named agents you talk to | 73 | for (const template of TEMPLATES) { |
| 74 | const made = applyChanges(null, { ...template, template: template.id }, TEMPLATE_IDS); | |
| 75 | assert.ok(made.ok, template.id); | |
| 76 | } | |
| 77 | const byId = Object.fromEntries(TEMPLATES.map((t) => [t.id, t])); | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 78 | assert.equal(byId.qa.routing.floor, "large", "careful review never runs below large"); |
| 79 | assert.equal(byId.product.routing.ceiling, "large", "intake never runs above it"); | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 80 | assert.equal(new Set(TEMPLATES.map((t) => t.handle)).size, TEMPLATES.length, "handles are distinct"); |
| 81 | }); | |
| 82 | ||
| 83 | test("members see a workspace's agents; owners change them", () => { | |
| 84 | const person = (role: "owner" | "member"): User => ({ id: "u", username: "u", workspaces: [{ slug: "acme", role }] }) as User; | |
| 85 | assert.ok(canSee(person("member"), "ACME")); | |
| 86 | assert.ok(!canManage(person("member"), "acme")); | |
| 87 | assert.ok(canManage(person("owner"), "acme")); | |
| 88 | assert.ok(!canSee(person("owner"), "other")); | |
| 89 | assert.ok(!canSee(null, "acme")); | |
| 90 | const token = { id: "w", username: "acme", kind: "workspace" } as User; | |
| 91 | assert.ok(canManage(token, "acme"), "the workspace's own token"); | |
| 92 | assert.ok(!canSee(token, "other")); | |
| 93 | }); | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 94 | |
| 95 | test("templates are named like teammates, with more names to shuffle, each a valid handle", async () => { | |
| 96 | const { checkHandle } = await import("./handle.ts"); | |
| 97 | const byId = Object.fromEntries(TEMPLATES.map((t) => [t.id, t])); | |
| 98 | assert.equal(byId.qa.display_name, "Margo"); | |
| 99 | assert.equal(byId.qa.handle, "margo"); | |
| 100 | for (const template of TEMPLATES) { | |
| 101 | assert.ok(!/reviewer|planner|triage|implementer|documenter|on-call|release/i.test(template.display_name), `${template.id} has a name, not a job title`); | |
| 102 | assert.ok(template.name_ideas.length >= 6 && template.name_ideas.length <= 10, template.id); | |
| 103 | assert.ok(template.name_ideas.includes(template.display_name), `${template.id}'s own name is among its ideas`); | |
| 104 | for (const name of template.name_ideas) assert.ok(checkHandle(name.toLowerCase()).ok, `${name} works as a handle`); | |
| 105 | assert.equal(new Set(template.name_ideas).size, template.name_ideas.length, `${template.id} repeats no name`); | |
| 106 | } | |
| 107 | }); | |
| 108 | ||
| 109 | test("an agent's avatar seed starts as its handle, can be changed, and stays through a rename", () => { | |
| 110 | const made = applyChanges(null, input, TEMPLATE_IDS); | |
| 111 | assert.ok(made.ok); | |
| 112 | if (!made.ok) return; | |
| 113 | assert.equal(made.value.avatar_seed, "ship"); | |
| 114 | const renamed = applyChanges(made.value, { handle: "skipper" }, TEMPLATE_IDS); | |
| 115 | assert.ok(renamed.ok); | |
| 116 | if (renamed.ok) assert.equal(renamed.value.avatar_seed, "ship", "a new name keeps the face"); | |
| 117 | const reseeded = applyChanges(made.value, { avatar_seed: "ship-2" }, TEMPLATE_IDS); | |
| 118 | assert.ok(reseeded.ok); | |
| 119 | if (reseeded.ok) assert.equal(reseeded.value.avatar_seed, "ship-2"); | |
| 120 | assert.equal(applyChanges(made.value, { avatar_seed: "x".repeat(65) }, TEMPLATE_IDS).ok, false); | |
| 121 | const chosen = applyChanges(null, { ...input, avatar_seed: "blob" }, TEMPLATE_IDS); | |
| 122 | assert.ok(chosen.ok && chosen.value.avatar_seed === "blob"); | |
| 123 | }); | |
| 124 | ||
| 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. | 125 | test("an agent is hired into a role: a title and responsibilities, never a team", async () => { |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 126 | const { roleOf } = await import("./definition.ts"); |
| 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. | 127 | const made = applyChanges(null, { handle: "margo", display_name: "Margo", title: "QA Engineer", instructions: "Test things.", responsibilities: ["Review pull requests", "Chase flaky checks"] }, TEMPLATE_IDS); |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 128 | assert.ok(made.ok); |
| 129 | if (!made.ok) return; | |
| 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. | 130 | assert.equal(made.value.role, "QA Engineer", "the one-line role is its title"); |
| 131 | assert.ok(!("team" in made.value) && !("department" in made.value), "teams are memberships, not part of the agent"); | |
| 132 | const retitled = applyChanges(made.value, { title: "QA Lead" }, TEMPLATE_IDS); | |
| 133 | assert.ok(retitled.ok && retitled.value.role === "QA Lead", "a made role follows the title"); | |
| 134 | const ignored = applyChanges(made.value, { team: "qa" } as never, TEMPLATE_IDS); | |
| 135 | assert.ok(ignored.ok && !("team" in ignored.value), "a team sent with a change is ignored"); | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 136 | const written = applyChanges(made.value, { role: "Keeps us honest" }, TEMPLATE_IDS); |
| 137 | assert.ok(written.ok); | |
| 138 | if (!written.ok) return; | |
| 139 | const kept = applyChanges(written.value, { title: "QA Lead" }, TEMPLATE_IDS); | |
| 140 | assert.ok(kept.ok && kept.value.role === "Keeps us honest", "a written role is kept"); | |
| 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. | 141 | assert.equal(roleOf({ title: "" }), ""); |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 142 | assert.equal(applyChanges(null, { handle: "x1", display_name: "X", instructions: "y" }, TEMPLATE_IDS).ok, false, "a title or a role is needed"); |
| 143 | const bad = (changes: object) => applyChanges(made.value, changes, TEMPLATE_IDS).ok; | |
| 144 | assert.equal(bad({ responsibilities: ["Only one"] }), false, "2 to 8 duties"); | |
| 145 | assert.equal(bad({ responsibilities: Array.from({ length: 9 }, (_, i) => `Duty ${i}`) }), false); | |
| 146 | assert.equal(bad({ responsibilities: [] }), true, "or none yet"); | |
| 147 | }); | |
| 148 | ||
| 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. | 149 | test("a role made from an agent's old team or department reads as its title", async () => { |
| 150 | const { definitionOf } = await import("./store.ts"); | |
| 151 | const row = (role: string, team: string | null, department: string) => | |
| 152 | definitionOf({ handle: "margo", display_name: "Margo", title: "QA Engineer", role, team, department, routing: "{}", budget: "{}", autonomy: "{}" } as never); | |
| 153 | assert.equal(row("QA Engineer on the qa team", "qa", "").role, "QA Engineer"); | |
| 154 | assert.equal(row("QA Engineer, QA", null, "QA").role, "QA Engineer"); | |
| 155 | assert.equal(row("Keeps us honest", "qa", "QA").role, "Keeps us honest", "a written role stays"); | |
| 156 | assert.ok(!("team" in row("x", "qa", "")), "the old columns are not part of the definition"); | |
| 157 | }); | |
| 158 | ||
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 159 | test("subagents: named, described, at most 8, never wider than their agent", () => { |
| 160 | const base = applyChanges(null, { ...input, routing: { floor: "large", ceiling: "large" } }, TEMPLATE_IDS); | |
| 161 | assert.ok(base.ok); | |
| 162 | if (!base.ok) return; | |
| 163 | const helper = { name: "flake-hunter", description: "Bisects flaky tests", instructions: "Find the cause.", routing: { floor: "small" as const, ceiling: "frontier" as const }, max_parallel: 3 }; | |
| 164 | const withHelper = applyChanges(base.value, { subagents: [helper] }, TEMPLATE_IDS); | |
| 165 | assert.ok(withHelper.ok); | |
| 166 | if (!withHelper.ok) return; | |
| 167 | assert.deepEqual(withHelper.value.subagents[0].routing, { floor: "large", ceiling: "large" }, "held within the agent's limits"); | |
| 168 | const loosened = applyChanges(withHelper.value, { routing: { floor: null, ceiling: null } }, TEMPLATE_IDS); | |
| 169 | assert.ok(loosened.ok && loosened.value.subagents[0].routing.floor === "large", "loosening the agent later does not widen what was stored"); | |
| 170 | const bad = (subagents: unknown) => applyChanges(base.value, { subagents: subagents as never }, TEMPLATE_IDS).ok; | |
| 171 | assert.equal(bad([{ ...helper, name: "Flake Hunter" }]), false); | |
| 172 | assert.equal(bad([helper, helper]), false, "names are unique"); | |
| 173 | assert.equal(bad([{ ...helper, description: "" }]), false); | |
| 174 | assert.equal(bad([{ ...helper, max_parallel: 9 }]), false); | |
| 175 | assert.equal(bad(Array.from({ length: 9 }, (_, i) => ({ ...helper, name: `helper-${i}` }))), false, "at most 8"); | |
| 176 | assert.equal(bad([{ ...helper, routing: { floor: "frontier", ceiling: "small" } }]), false); | |
| 177 | }); | |
| 178 | ||
| 179 | test("agents face the workspace's own people; customer-facing ones aren't available yet", () => { | |
| 180 | const made = applyChanges(null, input, TEMPLATE_IDS); | |
| 181 | assert.ok(made.ok && made.value.faces === "internal"); | |
| 182 | const customers = applyChanges(null, { ...input, faces: "customers" }, TEMPLATE_IDS); | |
| 183 | assert.ok(!customers.ok && customers.message === "Customer-facing agents aren't available yet."); | |
| 184 | }); | |
| 185 | ||
| 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. | 186 | test("role templates by title, each with duties and a subagent or two, on no team; David is back office", () => { |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 187 | const byId = Object.fromEntries(TEMPLATES.map((t) => [t.id, t])); |
| 188 | for (const template of TEMPLATES) { | |
| 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. | 189 | assert.ok(template.title && template.role === template.title, template.id); |
| 190 | assert.ok(!("department" in template) && !("team" in template), `${template.id} names a role, not a team`); | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 191 | assert.ok(template.responsibilities.length >= 2 && template.responsibilities.length <= 8, template.id); |
| 192 | assert.ok(template.subagents.length >= 1 && template.subagents.length <= 2, template.id); | |
| 193 | } | |
| 194 | assert.equal(byId.sales.display_name, "David"); | |
| 195 | assert.equal(byId.sales.title, "Sales Operations"); | |
| 196 | assert.match(byId.sales.instructions, /never contact a customer/i); | |
| 197 | assert.match(byId.sales.instructions, /never promise roadmap/i); | |
| 198 | assert.match(byId.sales.instructions, /audience/); | |
| 199 | assert.ok(!TEMPLATE_IDS.includes("planner"), "planning is g1t's own job"); | |
| 200 | }); | |
| 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. | 201 | |
| 202 | test("an agent's look is kept whole, checked part by part, and null goes back to the seed's face", () => { | |
| 203 | const made = applyChanges(null, input, TEMPLATE_IDS); | |
| 204 | assert.ok(made.ok); | |
| 205 | if (!made.ok) return; | |
| 206 | assert.equal(made.value.look, null, "a new agent wears its seed's face"); | |
| 207 | const look = { shape: "blob", color: "mint", eyes: "visor", mouth: "flat", antenna: "bulb", accessory: "headphones", pattern: "dots" } as const; | |
| 208 | const chosen = applyChanges(made.value, { look }, TEMPLATE_IDS); | |
| 209 | assert.ok(chosen.ok); | |
| 210 | if (!chosen.ok) return; | |
| 211 | assert.deepEqual(chosen.value.look, look); | |
| 212 | const renamed = applyChanges(chosen.value, { display_name: "Shipper" }, TEMPLATE_IDS); | |
| 213 | assert.ok(renamed.ok && renamed.value.look?.eyes === "visor", "a rename keeps the chosen face"); | |
| 214 | assert.equal(applyChanges(chosen.value, { look: { ...look, eyes: "laser" } as unknown as typeof look }, TEMPLATE_IDS).ok, false, "an unknown choice is refused"); | |
| 215 | assert.equal(applyChanges(chosen.value, { look: { shape: "round" } as unknown as typeof look }, TEMPLATE_IDS).ok, false, "a part missing is refused"); | |
| 216 | const back = applyChanges(chosen.value, { look: null }, TEMPLATE_IDS); | |
| 217 | assert.ok(back.ok && back.value.look === null); | |
| 218 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.