Skip to content

Commit

Merge branch 'rm-orgchart-getstarted'

syntaqxcommitted Parents9e4606f138bacdBrowse files
40 files+121−13450/40 viewed
+1−1
3737 | Chat: channels, direct messages and threads, live, with people and agents as members | Live |
3838 | Notifications: mentions, reviews, approvals and alerts, with browser push | Live |
3939 | People and teams: members, roles, nested teams, invites | Live |
40−| A directory of people and agents, profiles, org chart, teams with agent members | Coming |
40+| A directory of people and agents, profiles, teams with agent members | Coming |
4141 | Artifacts: documents written together, live, by people and agents | Live |
4242 | Slides, designs and dashboards as artifacts | Coming |
4343 | Scratchpads: tools, reports and agents anyone can build on a copy of the data, promoted after review | Coming |
+3−4
2323
2424 A workspace can have as many agents as it likes, and an agent costs nothing
2525 while nobody talks to it. Hire one from each template, add each to its
26−team, and a workspace's org chart can read like a real company's:
26+team, and a workspace's teams can read like a real company's:
2727
2828 | Team | Hired from the template | As |
2929 | --- | --- | --- |
312312 An agent's page in Agents is its only profile. The workspace's
313313 [People directory](/guides/people-and-teams/#the-directory) lists people
314314 only; agents appear there on the teams they are on, under the team's
315−people and marked **Agent**, and in the org chart beside the person who
316−leads their team.
315+people and marked **Agent**.
317316
318317 The Agents sidebar groups specialists by the teams they are on, each team
319318 by name: an agent on two teams shows under both, and agents on none show
505504 Every agent, not only g1t, knows the team: each colleague's name, title,
506505 the teams it is on, responsibilities and status. It also knows the people and agents on
507506 its own [teams](/guides/teams/): who leads each, what each person owns,
508−who reports to whom, who is around now and their local time, and who to
507+who is around now and their local time, and who to
509508 ask when a person is needed. It is told this every turn; see
510509 [what agents are told](/guides/people-and-teams/#what-agents-are-told).
511510 When a question belongs to someone else, it makes one of three moves,
+0−3
334334 too, and any project can show an uploaded image instead. See
335335 [production's picture](/guides/projects/#productions-picture).
336336
337−The [checklist on the overview](/guides/projects/#get-to-production) also
338−tracks the first production deploy, a custom domain and a first preview.
339−
340337 ## Deploying from a workflow
341338
342339 A [workflow](/guides/actions/) job can build and deploy the project
+10−33
11 ---
22 title: People and teams
3−description: Find anyone in a workspace, see what they own and who they report to, put agents on teams beside them, and know what agents are told about their teams.
3+description: Find anyone in a workspace, see what they own, put agents on teams beside them, and know what agents are told about their teams.
44 ---
55
66 **People** is where a workspace's people are listed: who they are, the
7−teams they are on, what they own and who they report to. Agents are not
8−in it: each has its page under [Agents](/guides/agents/), and agents join
7+teams they are on and what they own. Agents are not in it: each has its page under [Agents](/guides/agents/), and agents join
98 [teams](/guides/teams/) beside people. Open it from **People** at the foot
109 of the rail. Its sidebar has:
1110
1312 | --- | --- | --- |
1413 | **Everyone** | `g1t.sh/<workspace>/-/people` | The [directory](#the-directory) of the workspace's people. |
1514 | **Teams** | `g1t.sh/<workspace>/-/teams` | The workspace's [teams](/guides/teams/). |
16−| **Org chart** | `g1t.sh/<workspace>/-/org-chart` | Who reports to whom. See [the org chart](#the-org-chart). |
1715
1816 Only members of the workspace can open these pages. Who belongs, and
1917 inviting people, is the workspace's own business: **Members**,
4644 [Agents](/guides/agents/), and a team's page lists the agents on it under
4745 its people (see [agents on a team](/guides/teams/#agents-on-a-team)).
4846
49−**Org chart** opens [the org chart](#the-org-chart). Owners also see
50−**Invite people**, which opens the workspace's
47+Owners see **Invite people**, which opens the workspace's
5148 [Members](#members-invitations-and-permissions) page, where **Invite** is.
5249
5350 ## A person's profile
6360 | Card | What it shows |
6461 | --- | --- |
6562 | **Teams** | The teams they are on, with **Lead** or **Maintainer** beside each where it applies. |
66−| **Reporting line** | Who they report to, and who reports to them. |
6763 | **Agents on their teams** | Every agent on the teams they are on. |
6864 | **Access** | **Code**: their role on the workspace's repositories, such as *Admin on every repository, as an owner* or *Read on every repository, the workspace's base permission*, then any roles through teams, such as *Roles on 3 repositories through Backend*. **Storage**: coming. **Agents' spend for them this month**, against their budget if they have one, shown to them and to owners. |
6965
8177 | --- | --- | --- |
8278 | **Title** | The person, or an owner | Their job here, such as *Staff Engineer*. Up to 80 characters. |
8379 | **What they own** | The person, or an owner | One per line: a product, a process, an account. Up to 8, each up to 60 characters. Repeats are left out. |
84−| **Reports to** | Owners only | Another member of the workspace, or no one. Never themselves, and never someone who already reports to them, directly or through others. |
8580
8681 What you set here is shown in the directory and told to the agents on
8782 their teams (see [what agents are told](#what-agents-are-told)). Each
8883 change is recorded in the workspace's [audit log](/guides/audit-log/) as
8984 `member.profile_edited`.
9085
91−When someone leaves the workspace, anyone who reported to them reports to
92−no one, and any team they led has no lead.
86+When someone leaves the workspace, any team they led has no lead.
9387
9488 ## Agents
9589
9993 memory, routines, spend and activity, and its **Profile** tab, which has
10094 its definition and, under **Teams**, the teams it is on and
10195 [what it is told about them](#what-agents-are-told). Wherever People
102−names an agent, on a team's page, on a person's profile under **Agents
103−on their teams** or in the org chart, the name opens that page.
96+names an agent, on a team's page or on a person's profile under
97+**Agents on their teams**, the name opens that page.
10498
10599 Agents still join teams like anyone: see [teams of any mix](#teams-of-any-mix)
106100 below. The old address of an agent's People profile,
120114 | Add or remove an agent | The team's **People and agents** tab, or **Teams** on the agent's **Profile** tab in Agents. See [agents on a team](/guides/teams/#agents-on-a-team). |
121115 | Set the lead, channel and budget | The team's **Settings**. See [lead, channel and budget](/guides/teams/#lead-channel-and-budget). |
122116 | Storage for a team | Coming. |
123−
124−## The org chart
125117
126−The org chart, `g1t.sh/<workspace>/-/org-chart`, draws the reporting
127−lines as a tree: each person, with the people who report to them below.
128−Beside each person are the agents on the teams they lead, as members of
129−those teams: hover one for its name, title and that it is an agent, and
130−choose it to open its page in Agents. Agents have no place of their own
131−in the tree, since no one reports to an agent and an agent reports to no
132−one.
133−
134−Under **Teams no person leads** are the teams led by an agent or by no
135−one, each with its agents.
136−
137−Until someone has a manager, the page says so. Owners set who each person
138−reports to with **Edit profile** on that person's profile (see
139−[edit a profile](#edit-a-profile)).
140−
141118 ## What agents are told
142119
143120 Every time an agent replies, and at every step of a
150127 1. The team's description, who leads it, its channel and its shared
151128 budget for agents.
152129 2. Each person on it: name and username, title, whether they are the lead
153− or a maintainer, what they own, who they report to, how they show right
154− now, and their local time.
130+ or a maintainer, what they own, how they show right now, and their
131+ local time.
155132 3. Each agent on it, with *you* for itself.
156133 4. **When a person is needed**: who to ask.
157134
202179
203180 Payments, invoices and the billing pages. Led by Priya Shah (@priya). Its channel is #billing. Its agents share a budget of $150.00 a month.
204181
205−- Priya Shah (@priya); Engineering Manager, the lead, a maintainer; owns Stripe, Invoices; reports to @dana (online, 14:05 their time)
206−- Leo Park (@leo); Staff Engineer; owns Refunds; reports to @priya (focusing, in Do Not Disturb until 16:00 their time, 22:05 their time)
182+- Priya Shah (@priya); Engineering Manager, the lead, a maintainer; owns Stripe, Invoices (online, 14:05 their time)
183+- Leo Park (@leo); Staff Engineer; owns Refunds (focusing, in Do Not Disturb until 16:00 their time, 22:05 their time)
207184 - @margo: you
208185 - @sam (Sam), an agent: Support Specialist
209186
+3−49
3535
3636 | Page | Address | |
3737 | --- | --- | --- |
38−| **Overview** | `g1t.sh/<workspace>/<project>` | What it is and its links; production wherever it is deployed, or for a library its packages; the steps that apply to it; what is in progress, active branches, the latest commits, and its About. See [the overview](#the-overview). |
38+| **Overview** | `g1t.sh/<workspace>/<project>` | What it is and its links; production wherever it is deployed, or for a library its packages; what is in progress, active branches, the latest commits, and its About. See [the overview](#the-overview). |
3939 | **Code** | `…/code` | The repository's files, commits and branches. |
4040 | **Issues**, **Pull requests**, **Merge queue**, **Plan** | `…/issues` and so on | As they always were. |
4141 | **Actions** | `…/actions` | [GitHub Actions workflows](/guides/actions/). |
5757 | **Production** | [Production's picture](#productions-picture), which opens the live site, beside the rest. For an app [deployed on g1t](/guides/deployments/): its address, the commit it runs and when it went up; **Visit** and **Redeploy**. For an app deployed elsewhere: production's address and **Visit**, with its latest reported production deployment or the default branch's latest commit. For an app nobody has placed yet: one question, where it runs. |
5858 | **Packages** | For a library or a tool, in place of Production: the packages its repository publishes, each with its latest version and how to install it, or how to publish a first one. |
5959 | **Documentation**, **Homepage** | For docs, where they are read; for anything else, its homepage. |
60−| **Get to production**, **Ship a release** or **Get started** | The [checklist](#the-checklist) for what the project is, until every step is done or you dismiss it. |
6160 | **Right now** | Agents at work, and open pull requests moving from working to landed. |
6261 | **Needs you** | What is waiting on a person: a failed production build, a pull request to merge or review, a stuck run. |
6362 | **Active branches** | Branches other than the default, newest first. See [active branches](#active-branches). |
123122 Anyone who can change the project's settings (the **Maintain** role on
124123 its repository and up, as for production's address) can upload, replace
125124 or remove it.
126−
127−### The checklist
128−
129−People with a role on its repository see a card that counts what the
130−project has done, such as **3/5**. Its steps follow
131−[what the project is](#what-a-project-is): only an app deployed on g1t is
132−asked to deploy, add a domain or open a preview. Each step is worked out
133−from the project itself, and each links to where you do it. The card goes
134−away when every step is done. To hide it sooner, choose **×** on it. That
135−hides it for this project in this browser only.
136−
137−| What it is | Card | Steps |
138−| --- | --- | --- |
139−| App or site, deployed on g1t | **Get to production** | [Below](#get-to-production). |
140−| Library, tool | **Ship a release** | [Below](#ship-a-release). |
141−| App, deployed elsewhere | **Get started** | Push code; add production's address; add checks on pull requests; repository instructions; a first issue for g1t. |
142−| App nobody has placed yet | **Get started** | Push code; say where it runs; add checks on pull requests; repository instructions; a first issue for g1t. |
143−| Documentation | **Get started** | Push code; add where its docs are read (a docs or homepage address); repository instructions; a first issue for g1t. |
144−| Something else | **Get started** | Push code; add its links; repository instructions; a first issue for g1t. |
145−
146−### Get to production
147−
148−For an app or site deployed on g1t:
149125
150−| Step | Done when | Links to |
151−| --- | --- | --- |
152−| Connect a source or push code | The default branch has a commit, or the source is a mirror. | **Code**, which shows how to push, or how to have your coding agent start the project. |
153−| Deploy to production | A production build has gone live. | Deployments settings, or **Deployments** once they are on. |
154−| Add a custom domain | The project has a [custom domain](/guides/deployments/#custom-domains). | Domain settings. |
155−| Open a preview | A branch or pull request has had a [preview](/guides/deployments/#previews-of-branches). | A new pull request. |
156−| Set up repository instructions | `AGENTS.md` or `CLAUDE.md` is at the root of the default branch. | The instructions on the **Agents** page. See [repository instructions](/guides/working-with-g1t/#repository-instructions). |
157−| Assign a first issue to g1t | g1t has had a run, a pull request or an issue here. | A new issue. |
158−
159−### Ship a release
160−
161−A library or a tool does not deploy, so its card counts the steps to a
162−first release instead:
163−
164−| Step | Done when | Links to |
165−| --- | --- | --- |
166−| Connect a source or push code | As above. | **Code**. |
167−| Add checks on pull requests | The repository has a workflow in `.g1t/workflows`. Its runs are every pull request's checks. | **Actions**, which offers to add a starter workflow. |
168−| Tag a release or publish a package | A package its repository publishes has a version. For [Composer](/guides/composer/) and [Go](/guides/go/), pushing a tag such as `v1.0.0` is the release. | The package's page, or the guide for its registry. |
169−| Set up repository instructions | As above. | The **Agents** page. |
170−| Assign a first issue to g1t | As above. | A new issue. |
171−
172126 ## What a project is
173127
174128 A project is one of these, and its overview follows:
175129
176130 | What it is | Its overview shows |
177131 | --- | --- |
178−| **App or site, deployed on g1t** | Production on g1t.page, with previews and domains, and **Get to production**. |
132+| **App or site, deployed on g1t** | Production on g1t.page, with previews and domains. |
179133 | **App or site, deployed elsewhere** | Production at the address you give, deployed by your own pipeline, with **Visit**. It is never asked to turn on Deployments; Deployment settings stay one link away. |
180−| **Library or package** | The packages its repository publishes and how to install them, and **Ship a release**. |
134+| **Library or package** | The packages its repository publishes and how to install them. |
181135 | **Tool or CLI** | The same as a library: its packages and releases. |
182136 | **Documentation** | Where its docs are read. Docs can be published on g1t.page too, by turning on Deployments. |
183137 | **Something else** | Its homepage and links. |
+3−3
1515 Only members of a workspace, and its own agents, can be in its teams.
1616 Someone who leaves the workspace leaves all of its teams with it.
1717
18−For the workspace's directory, profiles and org chart, see
18+For the workspace's directory and profiles, see
1919 [people and teams](/guides/people-and-teams/).
2020
2121 ## Create a team
182182
183183 Every agent on a visible team is told about it every time it replies and
184184 at every session step: its description, lead, channel and budget, each
185−person with their title, what they own, who they report to, whether they
186−are around and their local time, and the team's other agents. The
185+person with their title, what they own, whether they are around and
186+their local time, and the team's other agents. The
187187 **People and agents** tab shows the team's part of it, exactly as the
188188 agents get it, under **What the agents on** *team* **know about it**.
189189
+1−2
571571 | [**Packages**](/guides/packages/) | `/-/packages` | Everyone | What the workspace publishes. A visitor opens it from **Packages** on the workspace's page. |
572572 | [**Teams**](/guides/teams/) | `/-/teams` | Members | Groups of members given roles on repositories together, mentioned as `@workspace/team` and asked to review together. Each team has its own page at `/-/teams/<team>`. |
573573 | [**People**](/guides/people-and-teams/) | `/-/people` | Members | The workspace's people, with their titles, teams and what they own. Each has a profile at `/-/people/<username>`. Agents are not in it; an agent's page is `/-/agents/<handle>`. |
574−| [**Org chart**](/guides/people-and-teams/#the-org-chart) | `/-/org-chart` | Members | Who reports to whom, with the agents on the teams each person leads beside them. |
575574 | **Members and invites** | `/-/members` | Members | Who belongs. Owners invite, change roles and remove people here. |
576575 | **Insights** | `/-/insights` | Members | Coming soon: how the whole workspace delivers. |
577576 | **Settings** | `/-/settings` | Owners | How the workspace is set up and connected (below). |
637636 | **Code** | Projects, pull requests and checks. Not shown to a member without [Code access](/guides/agent-access/). |
638637 | **Agents** | The workspace's agents and their sessions, and how they are set up. See [agents](/guides/agents/). |
639638 | **Artifacts** | Documents, decks and pages. See [artifacts](/guides/artifacts/). |
640−| **People** | Everyone in the workspace (its people; agents are under Agents); its [teams](/guides/teams/), which agents can be on; and the org chart. See [people and teams](/guides/people-and-teams/). |
639+| **People** | Everyone in the workspace (its people; agents are under Agents); and its [teams](/guides/teams/), which agents can be on. See [people and teams](/guides/people-and-teams/). |
641640 | **Workspace** | The workspace's settings: who has access, money, compute, and how each part of g1t is set up. See [Workspace mode](#workspace-mode). |
642641
643642 The app you are in sits on a filled square. Your account is the
+2−2
6666 | [Home](/guides/home/) | The front page: where you're needed now, what happened since you were last here, what is running, and where to start. | <Status is="preview" /> |
6767 | [Chat](/guides/chat/) | Channels, direct messages and threads, live. People and agents are members alike. No seats and no history limit. | <Status is="live" /> |
6868 | [Notifications](/guides/notifications/) | Everything waiting on you: mentions, reviews, approvals and alerts, with browser push. | <Status is="live" /> |
69−| [People and teams](/guides/people-and-teams/) | Everyone in the workspace in one search, with profiles, reporting lines, an org chart and teams that include agents beside people. Members, roles, nested teams and invites. | <Status is="live" /> |
69+| [People and teams](/guides/people-and-teams/) | Everyone in the workspace in one search, with profiles and teams that include agents beside people. Members, roles, nested teams and invites. | <Status is="live" /> |
7070 | [Artifacts](/guides/artifacts/) | Documents written together, live, by people and agents. Private until you share them. | <Status is="live" /> |
7171 | More kinds of artifact | Slides, designs and dashboards that keep the query behind every number. | <Status is="coming" /> |
7272 | Scratchpads | Tools, reports and agents anyone can build on a copy of the data, used in production only after review. | <Status is="coming" /> |
208208 />
209209 <LinkCard
210210 title="People and teams"
211− description="Find anyone, person or agent, see what they own and who they report to, and read what agents are told about their teams."
211+ description="Find anyone, person or agent, see what they own, and read what agents are told about their teams."
212212 href="/guides/people-and-teams/"
213213 />
214214 <LinkCard
+0−99
1−import { Check, ChevronRight, X } from "lucide-react";
2−import { useCallback, useSyncExternalStore } from "react";
3−import { Link } from "react-router";
4−
5−import { type ChecklistItem, dismiss, isDismissed, progress } from "../lib/checklist";
6−import { cn } from "../lib/cn";
7−import { Button } from "./ui/button";
8−import { Hint } from "./ui/hint";
9−
10−const storage = () => (typeof window === "undefined" ? null : window.localStorage);
11−const listeners = new Set<() => void>();
12−const subscribe = (listener: () => void) => {
13− listeners.add(listener);
14− return () => listeners.delete(listener);
15−};
16−
17−/**
18− * The steps to production, or to a library's first release, for one
19− * project, as `3/6`, until they are all done or someone dismisses it.
20− * Dismissing is per project and remembered by this browser.
21− */
22−export function ProductionChecklist({
23− base,
24− items,
25− title = "Get to production",
26−}: {
27− base: string;
28− items: ChecklistItem[];
29− /** "Ship a release" for a library. */
30− title?: string;
31−}) {
32− const hidden = useSyncExternalStore(
33− subscribe,
34− useCallback(() => isDismissed(storage, base), [base]),
35− () => false,
36− );
37− const { done, total, complete } = progress(items);
38− if (hidden || complete) return null;
39− const next = items.find((item) => !item.done)?.key;
40−
41− return (
42− <section aria-labelledby="checklist-title" className="rounded-2xl border border-line bg-surface">
43− <div className="flex items-center gap-3 px-5 pt-4 pb-3 sm:px-6">
44− <h2 id="checklist-title" className="text-sm font-semibold">
45− {title}
46− </h2>
47− <span className="rounded-full bg-raised px-2 py-px text-xs font-medium tabular-nums text-muted">
48− {done}/{total}
49− </span>
50− <span className="hidden h-1.5 w-24 overflow-hidden rounded-full bg-line sm:block" aria-hidden>
51− <span className="block h-full rounded-full bg-success" style={{ width: `${(done / total) * 100}%` }} />
52− </span>
53− <Hint label="Dismiss for this project">
54− <Button
55− type="button"
56− onClick={() => {
57− dismiss(storage, base);
58− for (const listener of listeners) listener();
59− }} variant="ghost" size="inline" className="ml-auto p-1 text-faint" aria-label="Dismiss the checklist for this project">
60− <X size={14} />
61− </Button>
62− </Hint>
63− </div>
64− <ol className="grid border-t border-line sm:grid-cols-2 lg:grid-cols-3">
65− {items.map((item) => (
66− <li key={item.key} className="border-b border-line last:border-b-0 sm:[&:nth-last-child(-n+2)]:border-b-0 lg:[&:nth-last-child(-n+3)]:border-b-0">
67− <Link
68− to={item.to}
69− className={cn(
70− "group flex h-full items-start gap-3 px-5 py-3 transition-colors hover:bg-raised/50 sm:px-6",
71− item.done && "opacity-70",
72− )}
73− >
74− <span
75− className={cn(
76− "mt-px flex size-4.5 shrink-0 items-center justify-center rounded-full border",
77− item.done ? "border-success bg-success text-bg" : item.key === next ? "border-success" : "border-line-strong",
78− )}
79− aria-hidden
80− >
81− {item.done && <Check size={11} strokeWidth={3} />}
82− </span>
83− <span className="min-w-0 grow">
84− <span className={cn("block text-[0.8125rem] font-medium", item.done && "text-muted line-through decoration-line-strong")}>
85− {item.title}
86− <span className="sr-only">{item.done ? " (done)" : ""}</span>
87− </span>
88− {!item.done && <span className="mt-0.5 block text-xs leading-5 text-muted">{item.detail}</span>}
89− </span>
90− {!item.done && (
91− <ChevronRight size={14} className="mt-0.5 shrink-0 text-faint transition-colors group-hover:text-fg" />
92− )}
93− </Link>
94− </li>
95− ))}
96− </ol>
97− </section>
98− );
99−}
+1−124
22 import { type ReactNode, useEffect, useState } from "react";
33 import { Link, useFetcher, useLocation, useRouteLoaderData, useSearchParams } from "react-router";
44
5−import { trialClosed } from "../lib/trial";
65 import { type ActivityGroup, type Verb, greetingFor, isAgent } from "../lib/mission";
76 import {
87 BLOCKING,
2827 import { cn } from "../lib/cn";
2928 import { microsOf, money } from "../lib/money";
3029 import { AgentComposer, type ComposerResult } from "./agent-composer";
31−import { AgentSetup } from "./agent-setup";
3230 import { InboxNeedsCard } from "./inbox";
3331 import type { ShellData } from "./shell";
3432 import { useLiveRefresh } from "./agents";
621619 </li>
622620 );
623621 }
624−
625−// --- Get started --------------------------------------------------------------------
626622
627−type Step = { done: boolean; title: string; about: string; to: string | null; action: string };
628−
629−/** The first things to do, ticked off as they are done, until work has been handed to agents. */
630−function GetStarted({ steps }: { steps: Step[] }) {
631− const left = steps.filter((step) => !step.done).length;
632− return (
633− <Card asChild className="p-5">
634− <section>
635− <div className="flex items-baseline justify-between gap-3">
636− <h2 className="text-base font-semibold tracking-tight">Get started</h2>
637− <span className="text-xs text-muted">
638− {steps.length - left} of {steps.length} done
639− </span>
640− </div>
641− <ol className="mt-4 space-y-2">
642− {steps.map((step, index) => (
643− <li
644− key={step.title}
645− className={cn("flex items-start gap-3 rounded-lg px-3 py-2.5", step.done ? "" : "bg-bg/50 ring-1 ring-line")}
646− >
647− <span
648− className={cn(
649− "mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full text-[0.6875rem] font-medium",
650− step.done ? "bg-success text-bg" : "text-muted ring-1 ring-line-strong",
651− )}
652− >
653− {step.done ? <Check size={12} /> : index + 1}
654− </span>
655− <span className="min-w-0 grow">
656− <span className={cn("block text-sm font-medium", step.done && "text-muted line-through decoration-faint")}>{step.title}</span>
657− {!step.done && <span className="mt-0.5 block text-xs leading-5 text-muted">{step.about}</span>}
658− </span>
659− {!step.done && step.to && (
660− <Link
661− to={step.to}
662− className="shrink-0 rounded-md bg-fg px-2.5 py-1 text-xs font-medium text-bg transition-colors hover:bg-fg-hover"
663− >
664− {step.action}
665− </Link>
666− )}
667− </li>
668− ))}
669− </ol>
670− </section>
671− </Card>
672− );
673−}
674−
675623 // --- The page -----------------------------------------------------------------------
676624
677625 const TAB_LABEL: Record<Tab, string> = { needs: "Needs you", waiting: "Waiting on agents", landed: "Landed today" };
709657 }
710658 }, []);
711659
712− const { viewer, repos, waiting, landed, liveTotal, week, groups, titles, stats, canRunAgents, trial } = loaded;
660+ const { viewer, repos, waiting, landed, liveTotal, week, groups, titles, stats, canRunAgents } = loaded;
713661 const workspace = shell?.workspace?.slug ?? loaded.workspace ?? viewer.workspaces?.[0]?.slug ?? null;
714662
715663 // The usage limit, as the sidebar knows it, heads the list when it bites.
761709 return text ? `?${text}` : pathname;
762710 };
763711
764− const steps: Step[] = [
765− {
766− done: Boolean(workspace),
767− title: "Create a workspace",
768− about: "Projects, people and agent credit live in one.",
769− to: "/workspaces/new",
770− action: "Create",
771− },
772− {
773− done: repos.length > 0,
774− title: "Add a project",
775− about: "Create a repository, or import one by its address. Push to it with git as usual.",
776− to: "/new",
777− action: "Add",
778− },
779− trial?.open
780− ? {
781− done: true,
782− title: "Try g1t free",
783− about: trial.granted
784− ? `This workspace has ${dollars(trial.limitMicros - trial.usedMicros)} of its ${dollars(trial.limitMicros)} trial credit left, for g1t's models and sandboxes.`
785− : `This workspace gets ${dollars(trial.limitMicros)} of trial credit, for g1t's models and sandboxes, the first time g1t works here.`,
786− to: workspace ? `/${workspace}/-/integrations/models` : null,
787− action: "Connect",
788− }
789− : canRunAgents
790− ? {
791− done: Boolean(shell?.free) || shell?.limit == null || shell.limit.comped || shell.limit.state === "ok",
792− title: "Keep work running",
793− about:
794− "Usage is charged after it runs. Add a card under Billing, so g1t charges it as you near your limit instead of stopping work.",
795− to: workspace ? `/${workspace}/-/billing` : null,
796− action: "Billing",
797− }
798− : {
799− done: false,
800− title: "Connect a model",
801− about: `${
802− trialClosed(trial, workspace ?? "This workspace") ? `${trialClosed(trial, workspace ?? "This workspace")} ` : ""
803− }Agents need a model to think with. Connect your Anthropic or OpenAI key, or any compatible endpoint.`,
804− to: workspace ? `/${workspace}/-/integrations/models` : null,
805− action: "Connect",
806− },
807− {
808− done: loaded.handedOff,
809− title: "Hand off an outcome",
810− about:
811− "Open an issue and assign it to g1t, or write an outcome on a project's Plans page and let a planner split it into issues.",
812− to: repos[0] ? `/${repos[0].namespace}/${repos[0].name}/plans` : null,
813− action: "Write one",
814− },
815− ];
816− const starting = steps.some((step) => !step.done);
817712 const noProjects = repos.length === 0;
818713
819714 const rows = tab === "needs" ? sortRows(needs, sort) : tab === "waiting" ? sortRows(waiting, sort) : sortRows(landed, sort);
921816 </Link>
922817 )}
923818 </section>
924−
925− {starting && <GetStarted steps={steps} />}
926819
927820 <Card asChild tone="plain" className="grid grid-cols-2 gap-px overflow-hidden bg-line lg:grid-cols-5">
928821 <section
12031096 ))}
12041097 </ol>
12051098 )}
1206− </section>
1207− </Card>
1208−
1209− <Card asChild className="p-5">
1210− <section>
1211− <h2 className="text-sm font-semibold">Connect your own agent</h2>
1212− <p className="mt-1.5 text-xs leading-5 text-muted">
1213− Add g1t to your coding agent. It signs in through your browser; there is no token to copy.
1214− </p>
1215− <AgentSetup className="mt-3" />
1216− <Link
1217− to="https://docs.g1t.sh/guides/bring-your-own-agent/"
1218− className="mt-3 inline-flex items-center gap-1 text-xs text-muted hover:text-fg"
1219− >
1220− How it works <ArrowRight size={12} />
1221− </Link>
12221099 </section>
12231100 </Card>
12241101 </aside>
+0−7
1313 import { DeploymentsPanel } from "./deployments-panel";
1414 import { CodeLines } from "./code-lines";
1515
16−import { AgentSetup } from "./agent-setup";
1716 import { useAddresses } from "../lib/addresses";
1817 import { isImagePath } from "../lib/usercontent";
1918 import { CloneBox } from "./clone-box";
356355 <CopyLine prompt text={`git remote add g1t ${cloneUrl}`} />
357356 <CopyLine prompt text={`git push -u g1t ${ref}`} />
358357 </div>
359− <h3 className="mt-7 text-sm font-medium">Or have your coding agent start it</h3>
360− <p className="mt-1 text-sm text-muted">
361− Connect it to g1t, then ask it to build the first version of{" "}
362− <span className="font-mono text-fg">{repo.name}</span> and push it here.
363− </p>
364− <AgentSetup className="mt-3" />
365358 </Card>
366359 );
367360 }
+2−7
13401340
13411341 /**
13421342 * People's sidebar: everyone in the workspace (its people; agents are
1343− * under Agents); its teams, which agents can be on; and the org chart. Who
1344− * belongs, and inviting people, is the workspace's: Workspace mode's
1345− * Access group.
1343+ * under Agents) and its teams, which agents can be on. Who belongs, and
1344+ * inviting people, is the workspace's: Workspace mode's Access group.
13461345 */
13471346 function PeopleSidebar({ slug }: { slug: string }) {
13481347 const { pathname } = useLocation();
13601359 <SidebarLink to={`/${slug}/-/teams`} icon={<UsersRound size={15} />} current={at("teams")}>
13611360 Teams
13621361 </SidebarLink>
1363− <SidebarLink to={`/${slug}/-/org-chart`} icon={<Network size={15} />} current={at("org-chart")}>
1364− Org chart
1365− </SidebarLink>
13661362 </NavRows>
13671363 </nav>
13681364 </div>
14611457 secrets: "Secrets and variables",
14621458 settings: "Settings",
14631459 people: "People",
1464− "org-chart": "Org chart",
14651460 members: "Members",
14661461 permissions: "Permissions",
14671462 projects: "Projects",
+1−1
7777
7878 Cloudflare may set its own security cookies (such as `__cf_bm`) to tell people from bots when g1t is under attack.
7979
80−The site also keeps a few preferences in your browser's local storage, which never leave your device: which coding agent you set up with, how you like diffs shown, and checklist items you've dismissed. HeyCatch keeps a copy of its analytics id there too, which is sent with each analytics event.
80+The site also keeps a few preferences in your browser's local storage, which never leave your device: which coding agent you set up with and how you like diffs shown. HeyCatch keeps a copy of its analytics id there too, which is sent with each analytics event.
8181
8282 **Fonts.** g1t.sh and docs.g1t.sh serve their typefaces themselves, so loading a page asks no one else for them.
8383
+0−174
1−import assert from "node:assert/strict";
2−import { test } from "node:test";
3−
4−import {
5− type ChecklistFacts,
6− agentWasAssigned,
7− checklistPlan,
8− dismiss,
9− dismissKey,
10− hasInstructions,
11− isDismissed,
12− productionChecklist,
13− progress,
14− releaseChecklist,
15− startChecklist,
16−} from "./checklist.ts";
17−
18−const fresh: ChecklistFacts = {
19− base: "/acme/web",
20− hasCode: false,
21− deploysEnabled: false,
22− productionDeployed: false,
23− domains: 0,
24− previewOpened: false,
25− instructions: false,
26− agentAssigned: false,
27−};
28−
29−test("a new project has everything to do, in order", () => {
30− const items = productionChecklist(fresh);
31− assert.deepEqual(
32− items.map((item) => item.key),
33− ["code", "deploy", "domain", "preview", "instructions", "agent"],
34− );
35− assert.deepEqual(progress(items), { done: 0, total: 6, complete: false });
36−});
37−
38−test("each step is done from its own fact", () => {
39− const items = productionChecklist({ ...fresh, hasCode: true, productionDeployed: true, domains: 2 });
40− assert.deepEqual(progress(items), { done: 3, total: 6, complete: false });
41− const all = productionChecklist({
42− ...fresh,
43− hasCode: true,
44− productionDeployed: true,
45− domains: 1,
46− previewOpened: true,
47− instructions: true,
48− agentAssigned: true,
49− });
50− assert.equal(progress(all).complete, true);
51−});
52−
53−test("unknown facts count as not done", () => {
54− const items = productionChecklist({ ...fresh, domains: null, instructions: null });
55− assert.equal(items.find((item) => item.key === "domain")?.done, false);
56− assert.equal(items.find((item) => item.key === "instructions")?.done, false);
57−});
58−
59−test("each step links to where it is done", () => {
60− const off = productionChecklist(fresh);
61− assert.equal(off.find((item) => item.key === "deploy")?.to, "/acme/web/settings/deployments");
62− const on = productionChecklist({ ...fresh, deploysEnabled: true });
63− assert.equal(on.find((item) => item.key === "deploy")?.to, "/acme/web/deployments");
64− assert.equal(on.find((item) => item.key === "domain")?.to, "/acme/web/settings/domains");
65− assert.equal(on.find((item) => item.key === "agent")?.to, "/acme/web/issues/new");
66−});
67−
68−test("instructions are AGENTS.md or CLAUDE.md at the root", () => {
69− assert.equal(hasInstructions(["README.md", "AGENTS.md"]), true);
70− assert.equal(hasInstructions(["claude.md"]), true);
71− assert.equal(hasInstructions(["README.md", "AGENTS.mdx", "docs"]), false);
72−});
73−
74−test("g1t counts as assigned from a run, a pull request or an issue", () => {
75− const none = { runAgents: ["claude-code"], pullAgents: ["claude-code"], issues: [{ assignees: ["ana"], agent: null }] };
76− assert.equal(agentWasAssigned(none), false);
77− assert.equal(agentWasAssigned({ ...none, runAgents: ["g1t"] }), true);
78− assert.equal(agentWasAssigned({ ...none, pullAgents: ["g1t"] }), true);
79− assert.equal(agentWasAssigned({ ...none, issues: [{ assignees: ["G1T"], agent: null }] }), true);
80− assert.equal(agentWasAssigned({ ...none, issues: [{ assignees: [], agent: "g1t" }] }), true);
81−});
82−
83−test("dismissing is remembered per project, and storage that throws is ignored", () => {
84− const map = new Map<string, string>();
85− const store = { getItem: (key: string) => map.get(key) ?? null, setItem: (key: string, value: string) => void map.set(key, value) };
86− assert.equal(isDismissed(() => store, "/acme/web"), false);
87− assert.equal(dismiss(() => store, "/Acme/Web"), true);
88− assert.equal(isDismissed(() => store, "/acme/web"), true);
89− assert.equal(isDismissed(() => store, "/acme/api"), false);
90− assert.equal(dismissKey("/Acme/Web"), "g1t:checklist-dismissed:/acme/web");
91− const blocked = {
92− getItem: () => {
93− throw new Error("blocked");
94− },
95− setItem: () => {
96− throw new Error("blocked");
97− },
98− };
99− assert.equal(isDismissed(() => blocked, "/acme/web"), false);
100− assert.equal(dismiss(() => blocked, "/acme/web"), false);
101−});
102−
103−test("a library's checklist ships a release instead of deploying", () => {
104− const facts = {
105− base: "/flagon-io/php-log",
106− hasCode: true,
107− instructions: false,
108− agentAssigned: false,
109− hasWorkflow: false,
110− released: true,
111− releaseTo: "/flagon-io/-/packages/composer/psr/log",
112− };
113− const items = releaseChecklist(facts);
114− assert.deepEqual(
115− items.map((item) => item.key),
116− ["code", "checks", "release", "instructions", "agent"],
117− );
118− assert.deepEqual(progress(items), { done: 2, total: 5, complete: false });
119− assert.equal(items.find((item) => item.key === "checks")?.to, "/flagon-io/php-log/actions");
120− assert.equal(items.find((item) => item.key === "release")?.to, "/flagon-io/-/packages/composer/psr/log");
121− assert.ok(items.every((item) => !/deploy|production|domain|preview/i.test(item.title)));
122− // An unknown workflow list counts as not done.
123− assert.equal(releaseChecklist({ ...facts, hasWorkflow: null }).find((item) => item.key === "checks")?.done, false);
124−});
125−
126−const start = {
127− base: "/flagon-io/g1t",
128− hasCode: true,
129− instructions: true,
130− agentAssigned: false,
131− hasWorkflow: true,
132− productionUrl: null,
133− hasLinks: false,
134− hasDocsLink: false,
135−};
136−
137−test("only what g1t deploys gets production's steps", () => {
138− assert.deepEqual(checklistPlan({ kind: "app", runs: "g1t" }), { plan: "production", title: "Get to production" });
139− assert.deepEqual(checklistPlan({ kind: "docs", runs: "g1t" }).plan, "production");
140− assert.deepEqual(checklistPlan({ kind: "app", runs: "elsewhere" }), { plan: "elsewhere", title: "Get started" });
141− assert.equal(checklistPlan({ kind: "app", runs: null }).plan, "unknown");
142− assert.deepEqual(checklistPlan({ kind: "tool", runs: null }), { plan: "release", title: "Ship a release" });
143− assert.equal(checklistPlan({ kind: "library", runs: null }).plan, "release");
144− assert.equal(checklistPlan({ kind: "docs", runs: "elsewhere" }).plan, "docs");
145− assert.equal(checklistPlan({ kind: "other", runs: null }).plan, "other");
146−});
147−
148−test("an app deployed elsewhere is never asked to deploy on g1t", () => {
149− const items = startChecklist("elsewhere", start);
150− assert.deepEqual(
151− items.map((item) => item.key),
152− ["code", "production", "checks", "instructions", "agent"],
153− );
154− assert.ok(items.every((item) => !/deploy to|turn on|domain|preview/i.test(`${item.title} ${item.detail}`)));
155− assert.equal(items.find((item) => item.key === "production")?.done, false);
156− assert.equal(startChecklist("elsewhere", { ...start, productionUrl: "https://g1t.sh" }).find((item) => item.key === "production")?.done, true);
157− // Every step can be done.
158− const all = startChecklist("elsewhere", { ...start, productionUrl: "https://g1t.sh", agentAssigned: true });
159− assert.equal(progress(all).complete, true);
160−});
161−
162−test("an app nobody has placed asks where it runs; docs and other ask for links", () => {
163− assert.deepEqual(
164− startChecklist("unknown", start).map((item) => item.key),
165− ["code", "where", "checks", "instructions", "agent"],
166− );
167− assert.equal(startChecklist("unknown", start).find((item) => item.key === "where")?.to, "/flagon-io/g1t/settings#kind");
168− const docs = startChecklist("docs", { ...start, hasDocsLink: true });
169− assert.deepEqual(docs.map((item) => item.key), ["code", "links", "instructions", "agent"]);
170− assert.equal(docs.find((item) => item.key === "links")?.done, true);
171− const other = startChecklist("other", start);
172− assert.equal(other.find((item) => item.key === "links")?.done, false);
173− assert.equal(startChecklist("other", { ...start, hasLinks: true, agentAssigned: true }).every((item) => item.done), true);
174−});
+0−282
1−/**
2− * The steps that take a project to production, or for a library to its
3− * first release, each worked out from what the project has done, with
4− * where to do the ones it has not. Shown on the project's overview to its
5− * members until they are all done or it is dismissed.
6− */
7−
8−export type ChecklistFacts = {
9− /** The project's address, `/<workspace>/<project>`. */
10− base: string;
11− /** Its default branch has a commit, or its source is a mirror. */
12− hasCode: boolean;
13− /** Deployments are switched on for it. */
14− deploysEnabled: boolean;
15− /** A production build has gone live, now or before. */
16− productionDeployed: boolean;
17− /** Custom domains it has; null when they could not be read. */
18− domains: number | null;
19− /** A preview has been built for a branch or pull request. */
20− previewOpened: boolean;
21− /** An AGENTS.md or CLAUDE.md at the root of its default branch; null when unknown. */
22− instructions: boolean | null;
23− /** g1t has been given an issue here. */
24− agentAssigned: boolean;
25−};
26−
27−export type ChecklistItem = {
28− key: "code" | "deploy" | "domain" | "preview" | "checks" | "release" | "production" | "where" | "links" | "instructions" | "agent";
29− title: string;
30− detail: string;
31− done: boolean;
32− to: string;
33− action: string;
34−};
35−
36−export function productionChecklist(facts: ChecklistFacts): ChecklistItem[] {
37− const { base } = facts;
38− return [
39− {
40− key: "code",
41− title: "Connect a source or push code",
42− detail: "Push an existing project, or have your coding agent start one.",
43− done: facts.hasCode,
44− to: `${base}/code`,
45− action: "Push code",
46− },
47− {
48− key: "deploy",
49− title: "Deploy to production",
50− detail: facts.deploysEnabled
51− ? "Production builds from the default branch on every push."
52− : "Deployments are off until you turn them on. Then production builds from the default branch on every push.",
53− done: facts.productionDeployed,
54− to: facts.deploysEnabled ? `${base}/deployments` : `${base}/settings/deployments`,
55− action: facts.deploysEnabled ? "Deployments" : "Turn on",
56− },
57− {
58− key: "domain",
59− title: "Add a custom domain",
60− detail: "Serve production from a domain of your own.",
61− done: (facts.domains ?? 0) > 0,
62− to: `${base}/settings/domains`,
63− action: "Add",
64− },
65− {
66− key: "preview",
67− title: "Open a preview",
68− detail: "Every pull request gets a live preview of its branch.",
69− done: facts.previewOpened,
70− to: `${base}/pulls/new`,
71− action: "New pull request",
72− },
73− {
74− key: "instructions",
75− title: "Set up repository instructions",
76− detail: "Commit an AGENTS.md with how to build and test; every agent run reads it.",
77− done: facts.instructions === true,
78− to: `${base}/agents#instructions`,
79− action: "How",
80− },
81− {
82− key: "agent",
83− title: "Assign a first issue to g1t",
84− detail: "It opens a pull request, makes the change and sees it through checks and review.",
85− done: facts.agentAssigned,
86− to: `${base}/issues/new`,
87− action: "New issue",
88− },
89− ];
90−}
91−
92−export type ReleaseFacts = Pick<ChecklistFacts, "base" | "hasCode" | "instructions" | "agentAssigned"> & {
93− /** It has a workflow, whose runs are its pull requests' checks; null when unknown. */
94− hasWorkflow: boolean | null;
95− /** A package its repository publishes has a version. */
96− released: boolean;
97− /** Where publishing a first version is explained: its package's page or its registry's guide. */
98− releaseTo: string;
99−};
100−
101−/**
102− * The steps for a library or a tool, which ships as releases rather than
103− * deploying: the same first and last steps as production, with checks and
104− * a first version in between.
105− */
106−export function releaseChecklist(facts: ReleaseFacts): ChecklistItem[] {
107− const shared = productionChecklist({ ...facts, deploysEnabled: false, productionDeployed: false, domains: null, previewOpened: false });
108− const step = (key: ChecklistItem["key"]) => shared.find((item) => item.key === key)!;
109− return [
110− step("code"),
111− {
112− key: "checks",
113− title: "Add checks on pull requests",
114− detail: "A workflow that builds and tests it. Its runs are every pull request's checks.",
115− done: facts.hasWorkflow === true,
116− to: `${facts.base}/actions`,
117− action: "Add CI",
118− },
119− {
120− key: "release",
121− title: "Tag a release or publish a package",
122− detail: "Publish a first version to the workspace's registry for others to install.",
123− done: facts.released,
124− to: facts.releaseTo,
125− action: "How",
126− },
127− step("instructions"),
128− step("agent"),
129− ];
130−}
131−
132−export type StartFacts = Pick<ChecklistFacts, "base" | "hasCode" | "instructions" | "agentAssigned"> & {
133− /** It has a workflow, whose runs are its pull requests' checks; null when unknown. */
134− hasWorkflow: boolean | null;
135− /** Production's address, for an app deployed elsewhere. */
136− productionUrl: string | null;
137− /** Its homepage, docs or any other link is set. */
138− hasLinks: boolean;
139− /** Its docs address is set, or its homepage. */
140− hasDocsLink: boolean;
141−};
142−
143−/** Where a project's own settings are, for the steps that are done there. */
144−const settingsAt = (base: string, anchor: string) => `${base}/settings#${anchor}`;
145−
146−/**
147− * The steps for a project g1t does not deploy, by what it is. Every step
148− * applies to it and each is done from its own fact: nothing about turning
149− * on Deployments, domains or previews.
150− *
151− * - An app deployed elsewhere: its production address, then checks.
152− * - An app nobody has said where it runs: saying so, then checks.
153− * - Docs: where they are read.
154− * - Anything else: its links.
155− */
156−export function startChecklist(kind: "elsewhere" | "unknown" | "docs" | "other", facts: StartFacts): ChecklistItem[] {
157− const shared = productionChecklist({ ...facts, deploysEnabled: false, productionDeployed: false, domains: null, previewOpened: false });
158− const step = (key: ChecklistItem["key"]) => shared.find((item) => item.key === key)!;
159− const checks: ChecklistItem = {
160− key: "checks",
161− title: "Add checks on pull requests",
162− detail: "A workflow that builds and tests it. Its runs are every pull request's checks.",
163− done: facts.hasWorkflow === true,
164− to: `${facts.base}/actions`,
165− action: "Add CI",
166− };
167− const middle: ChecklistItem[] =
168− kind === "elsewhere"
169− ? [
170− {
171− key: "production",
172− title: "Add production's address",
173− detail: "Where your own pipeline deploys it, so its overview links to production.",
174− done: facts.productionUrl != null,
175− to: settingsAt(facts.base, "kind"),
176− action: "Add",
177− },
178− checks,
179− ]
180− : kind === "unknown"
181− ? [
182− {
183− key: "where",
184− title: "Say where it runs",
185− detail: "Deployed on g1t, deployed elsewhere, or not deployed at all, such as a library.",
186− done: false,
187− to: settingsAt(facts.base, "kind"),
188− action: "Choose",
189− },
190− checks,
191− ]
192− : kind === "docs"
193− ? [
194− {
195− key: "links",
196− title: "Add where its docs are read",
197− detail: "A docs or homepage address, shown on its overview and wherever the project is listed.",
198− done: facts.hasDocsLink,
199− to: settingsAt(facts.base, "links"),
200− action: "Add",
201− },
202− ]
203− : [
204− {
205− key: "links",
206− title: "Add its links",
207− detail: "A homepage, docs, or any other address people go to for it.",
208− done: facts.hasLinks,
209− to: settingsAt(facts.base, "links"),
210− action: "Add",
211− },
212− ];
213− return [step("code"), ...middle, step("instructions"), step("agent")];
214−}
215−
216−export type ChecklistPlan = "production" | "release" | "elsewhere" | "unknown" | "docs" | "other";
217−
218−/**
219− * Which steps a project gets, and their heading, from what it is and where
220− * it runs. Only what g1t deploys gets production's steps.
221− */
222−export function checklistPlan(project: {
223− kind: "app" | "library" | "tool" | "docs" | "other";
224− runs: "g1t" | "elsewhere" | null;
225−}): { plan: ChecklistPlan; title: string } {
226− if (project.runs === "g1t") return { plan: "production", title: "Get to production" };
227− if (project.kind === "library" || project.kind === "tool") return { plan: "release", title: "Ship a release" };
228− if (project.kind === "app") return { plan: project.runs === "elsewhere" ? "elsewhere" : "unknown", title: "Get started" };
229− return { plan: project.kind, title: "Get started" };
230−}
231−
232−/** `3/6`, for the card's heading. */
233−export function progress(items: ChecklistItem[]): { done: number; total: number; complete: boolean } {
234− const done = items.filter((item) => item.done).length;
235− return { done, total: items.length, complete: done === items.length };
236−}
237−
238−/** A repository's root lists instructions for agents. */
239−export function hasInstructions(names: string[]): boolean {
240− return names.some((name) => /^(agents|claude)\.md$/i.test(name));
241−}
242−
243−/** Whether g1t has worked here, from its runs, pull requests or issues. */
244−export function agentWasAssigned(input: {
245− runAgents: string[];
246− pullAgents: string[];
247− issues: { assignees: string[]; agent: string | null }[];
248−}): boolean {
249− const isAgent = (name: string | null) => name?.toLowerCase() === "g1t";
250− return (
251− input.runAgents.some(isAgent) ||
252− input.pullAgents.some(isAgent) ||
253− input.issues.some((issue) => isAgent(issue.agent) || issue.assignees.some(isAgent))
254− );
255−}
256−
257−// --- Dismissing it, per project, in this browser -----------------------------
258−
259−type Store = Pick<Storage, "getItem" | "setItem">;
260−
261−export function dismissKey(base: string): string {
262− return `g1t:checklist-dismissed:${base.toLowerCase()}`;
263−}
264−
265−export function isDismissed(storage: () => Store | null | undefined, base: string): boolean {
266− try {
267− return storage()?.getItem(dismissKey(base)) === "1";
268− } catch {
269− return false;
270− }
271−}
272−
273−export function dismiss(storage: () => Store | null | undefined, base: string): boolean {
274− try {
275− const store = storage();
276− if (!store) return false;
277− store.setItem(dismissKey(base), "1");
278− return true;
279− } catch {
280− return false;
281− }
282−}
+0−2
623623 workspace: slug,
624624 repos: repoList.filter((repo) => inWorkspace(repo)).map(pathOf),
625625 canRunAgents: models_ == null || models_.hosted || models_.own != null,
626− trial: models_?.own == null ? (models_?.trial ?? null) : null,
627− handedOff: activeList.length > 0 || runList.length > 0 || merged.length > 0,
628626 needs: needRows,
629627 waiting,
630628 landed,
+3−30
1313 budgetFromText,
1414 codeAccessWords,
1515 directoryEntries,
16− managerChoices,
17− orgChart,
1816 ownsFromText,
1917 teamBlock,
2018 teamsOfAgent,
3129 timezone: null,
3230 role: "member",
3331 title: null,
34− manager: null,
3532 owns: [],
3633 joined_at: "2026-10-01T00:00:00Z",
3734 ...extra,
7067 const directory: PeopleDirectory = {
7168 people: [
7269 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" }),
70+ person("sofia", { name: "Sofia Reyes", title: "Head of Sales", owns: ["the forecast"] }),
71+ person("jordan", { name: "Jordan Lee", title: "Account Executive", owns: ["Halcyon", "Bluebird"] }),
72+ person("kai", { name: "Kai Nakamura", title: "Product Designer" }),
7673 ],
7774 teams: [
7875 team("sales", "Sales", {
136133
137134 test("an agent's profile is its page in Agents, where People links to it", () => {
138135 assert.equal(agentPath("acme", "margo"), "/acme/-/agents/margo");
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.
150− assert.deepEqual(chart.unled.map((u) => [u.team.slug, u.agents.map((a) => a.handle)]), [["night-shift", ["atlas", "sentinel", "david"]]]);
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"]);
163136 });
164137
165138 test("forms read plainly", () => {
+1−63
11 /**
22 * 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.
3+ * teams and under Agents), profiles, and teams of any mix.
44 * Types only from the contracts, so this file runs under `node --test`.
55 */
66 import type { DirectoryPerson, DirectoryTeam, PeopleDirectory, TeamLead, WorkspaceAgent } from "@g1t/contracts";
119119 if (matches(query, fields)) out.push({ person, teams });
120120 }
121121 return out;
122−}
123−
124−/** One person in the org chart, with who reports to them and the agents of the teams they lead. */
125−export type OrgNode = { person: DirectoryPerson; agents: PeopleAgent[]; leads: DirectoryTeam[]; reports: OrgNode[] };
126−
127−export 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− */
139−export 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. */
166−export 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. */
171−export 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));
184122 }
185123
186124 /** What someone owns, from a form's text: one per line or comma. */
+12−0
1212 libraryPackages,
1313 neverDeploys,
1414 primaryLink,
15+ projectPlan,
1516 projectChanges,
1617 linksFromForm,
1718 linksToShow,
147148 { label: "", url: "npmjs.com/x" },
148149 ]);
149150 });
151+
152+test("only what g1t deploys leads with production", () => {
153+ assert.equal(projectPlan({ kind: "app", runs: "g1t" }), "production");
154+ assert.equal(projectPlan({ kind: "docs", runs: "g1t" }), "production");
155+ assert.equal(projectPlan({ kind: "app", runs: "elsewhere" }), "elsewhere");
156+ assert.equal(projectPlan({ kind: "app", runs: null }), "unknown");
157+ assert.equal(projectPlan({ kind: "tool", runs: null }), "release");
158+ assert.equal(projectPlan({ kind: "library", runs: null }), "release");
159+ assert.equal(projectPlan({ kind: "docs", runs: "elsewhere" }), "docs");
160+ assert.equal(projectPlan({ kind: "other", runs: null }), "other");
161+});
+14−0
8989 }
9090 }
9191
92+/** What a project's overview leads with: production, a release, or what it says of itself. */
93+export type ProjectPlan = "production" | "release" | "elsewhere" | "unknown" | "docs" | "other";
94+
95+/**
96+ * Which head a project's overview shows, from what it is and where it
97+ * runs. Only what g1t deploys gets production's.
98+ */
99+export function projectPlan(project: Pick<Project, "kind" | "runs">): ProjectPlan {
100+ if (project.runs === "g1t") return "production";
101+ if (project.kind === "library" || project.kind === "tool") return "release";
102+ if (project.kind === "app") return project.runs === "elsewhere" ? "elsewhere" : "unknown";
103+ return project.kind;
104+}
105+
92106 /** A link as pages list it: what it is, its label and address. */
93107 export type ShownLink = { key: string; type: "homepage" | "docs" | "production" | "custom"; label: string; url: string };
94108
+0−1
106106 assert.equal(modeOf("/acme/-/people/ana", "acme"), "people");
107107 // An agent's old People address (it redirects to Agents) is still read as People's until it does.
108108 assert.equal(modeOf("/acme/-/people/agents/margo", "acme"), "people");
109− assert.equal(modeOf("/acme/-/org-chart", "acme"), "people");
110109 // Members, invitations and permissions are the workspace's settings.
111110 assert.equal(modeOf("/acme/-/members", "acme"), "workspace");
112111 assert.equal(modeOf("/acme/-/members/invitations", "acme"), "workspace");
+0−1
317317 members: "workspace",
318318 permissions: "workspace",
319319 teams: "people",
320− "org-chart": "people",
321320 workspace: "workspace",
322321 spend: "workspace",
323322 usage: "workspace",
+1−2
100100 index("routes/workspace/overview.tsx"),
101101 // Its pages, each in the sidebar: Overview (above), Projects, People, Insights; Teams and Packages below.
102102 route("-/projects", "routes/workspace/projects.tsx"),
103− // People mode: the directory of people, each one's profile, and the org chart. Agents
103+ // People mode: the directory of people and each one's profile. Agents
104104 // are not in it; an agent's old People address redirects to its page in Agents.
105105 route("-/people", "routes/workspace/people.tsx"),
106106 route("-/people/agents/:handle", "routes/workspace/people/agent.tsx"),
107107 route("-/people/:username", "routes/workspace/people/person.tsx"),
108− route("-/org-chart", "routes/workspace/org-chart.tsx"),
109108 // Workspace mode's Access: who is a member and their roles, the invitations sent, and who may do what.
110109 route("-/members", "routes/workspace/members.tsx"),
111110 route("-/members/invitations", "routes/workspace/member-invitations.tsx"),
+2−2
4747 {!overview ? (
4848 <Card className="p-6 text-sm text-muted">Observability could not be loaded just now. Reload the page to try again.</Card>
4949 ) : nothing ? (
50− <GetStarted base={base} canManage={canManage} />
50+ <NothingYet base={base} canManage={canManage} />
5151 ) : (
5252 <Loaded overview={overview} errors={errors} base={base} path={path} view={view} />
5353 )}
314314 }
315315
316316 /** Nothing has arrived yet: the two ways it does. */
317−function GetStarted({ base, canManage }: { base: string; canManage: boolean }) {
317+function NothingYet({ base, canManage }: { base: string; canManage: boolean }) {
318318 return (
319319 <Card asChild>
320320 <section>
+12−68
4545 import { Elapsed, formatCost, useLiveRefresh } from "../../components/agents";
4646 import { ActivityFeed, DeployStrip, Meter, NeedsList, Panel, Quiet, Unavailable, percent } from "../../components/mission";
4747 import { type ActiveBranch, ActiveBranches } from "../../components/branches";
48−import { ProductionChecklist } from "../../components/checklist";
4948 import { PackageIcon } from "../../components/package-icon";
5049 import { Hint } from "../../components/ui/hint";
5150 import { Loading, Skeleton, SkeletonRows } from "../../components/ui/skeleton";
7978 runHealth,
8079 stuckMinutes,
8180 } from "../../lib/mission";
82−import { agentWasAssigned, checklistPlan, hasInstructions, productionChecklist, releaseChecklist, startChecklist } from "../../lib/checklist";
8381 import { ECOSYSTEM_LABEL, installCommands } from "../../lib/packages";
8482 import {
8583 PUBLISH_GUIDES,
8684 bare,
87− hasRelease,
8885 kindLabel,
8986 linksToShow,
9087 latestTag,
9289 packageName,
9390 packagePath,
9491 projectChanges,
92+ projectPlan,
9593 publishGuide,
9694 } from "../../lib/project-kind";
9795 import { AboutEditor, KindMenu, LinkList } from "../../components/project-about";
150148 const soft = <T,>(promise: Promise<T> | null): Promise<T | null> =>
151149 promise ? promise.catch((error) => (console.warn("overview:", error), null)) : Promise.resolve(null);
152150 // People with a role of their own here see its running parts: deployments,
153− // memory, setup; everyone who can read it sees the rest. Not awaited: only
151+ // memory; everyone who can read it sees the rest. Not awaited: only
154152 // those sections wait on the repository lookup, which the layout makes in
155153 // this request too.
156154 const accessP = accessTo(context, params);
159157 memberP.then((member) => (member ? soft(start()) : null));
160158 const repoP = soft(repoFor(context, params));
161159 const projectP = soft(projectFor(context, params));
162− // A library or a tool shows its packages where an app shows production,
163− // and every project g1t does not deploy counts a workflow in its checklist.
160+ // A library or a tool shows its packages where an app shows production.
164161 // Deployments from anywhere (g1t.page, g1t Actions, the API), by environment, for anyone who can read it.
165162 const environmentsP = projectP.then((found) =>
166163 soft(deployments.environments(found?.ok && found.value.source.kind === "hosted" ? found.value.source.repo : path, viewer)),
167164 );
168− const planP = projectP.then((project) => (project?.ok ? checklistPlan(project.value).plan : null));
165+ const planP = projectP.then((project) => (project?.ok ? projectPlan(project.value) : null));
169166 const libraryRepoP = Promise.all([planP, repoP]).then(([plan, repo]) => (plan === "release" && repo?.ok ? repo.value : null));
170167 const packagesP = libraryRepoP.then((repo) => (repo ? soft(packages.list(params.owner, viewer, { repoId: repo.id })) : null));
171− const workflowsP = planP.then((plan) => (plan && plan !== "production" ? forMembers(() => actions.workflows(path, viewer)) : null));
172168 // Recent workflow runs, for Health when no pull request's checks have finished lately.
173169 const workflowRunsP = soft(actions.runs(path, viewer, { limit: 30 }));
174170 // Only the kinds the feed shows: the newest events are mostly session
218214 branchesP.catch(() => null),
219215 new Promise<"slow">((resolve) => setTimeout(() => resolve("slow"), crawler ? BRANCHES_WAIT_CRAWLER_MS : BRANCHES_WAIT_MS)),
220216 ]);
221− const [{ insider: member, can }, project, settings, list, open, closed, log, counts, runs, queue, issues, memories, recent, mine, domains, root, packageList, workflows, tagList] = await Promise.all([
217+ const [{ insider: member, can }, project, settings, list, open, closed, log, counts, runs, queue, issues, memories, recent, mine, packageList, tagList] = await Promise.all([
222218 accessP,
223219 projectP,
224220 forMembers(() => deployments.settings(ref, viewer)),
234230 forMembers(() => agents.listMemories(viewer, params.owner, path)),
235231 eventsP,
236232 minePullsP,
237− // For the checklist, which only members see.
238− forMembers(() => deployments.domains(ref, viewer)),
239− forMembers(() => repos.tree(path, viewer, null, "")),
240233 packagesP,
241− workflowsP,
242234 // The latest release, for About.
243235 soft(repos.tags(path, viewer)),
244236 ]);
367359 const knows = distinctFacts([...memoryList.filter((m) => m.pinned), ...memoryList.filter((m) => !m.pinned).sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))]).slice(0, 5);
368360 const openIssues = ok(issues);
369361
370− // --- Getting to production ----------------------------------------------------
371− const wentLive = (status: string) => status === "ready" || status === "replaced" || status === "down";
372− const liveApps = ok(list)?.live ?? [];
362+ // --- Where it runs ------------------------------------------------------------
373363 const commitNow = ok(log)?.[0] ?? null;
374364 // Production deployed somewhere other than g1t.page (by g1t Actions, or
375365 // reported through the API): the slot an app deployed elsewhere shows.
380370 // elsewhere, runs elsewhere.
381371 const found = ok(project);
382372 const projectValue = found && deployment && found.kind === "app" && found.runs == null ? { ...found, runs: "elsewhere" as const } : found;
383− const plan = projectValue ? checklistPlan(projectValue) : { plan: "production" as const, title: "Get to production" };
384− const isLibrary = plan.plan === "release";
373+ const isLibrary = projectValue != null && projectPlan(projectValue) === "release";
385374 const published = libraryPackages(ok(packageList) ?? []);
386− const hasWorkflow = workflows?.ok ? workflows.value.length > 0 : null;
387− const firstSteps = {
388− base,
389− hasCode: commitNow != null || projectValue?.source.kind === "mirror",
390− instructions: commitNow == null ? false : root?.ok ? hasInstructions(root.value.entries.map((entry) => entry.name)) : null,
391− agentAssigned: agentWasAssigned({
392− runAgents: runList.map((run) => run.agent),
393− pullAgents: [...openPulls, ...(ok(closed) ?? [])].map((pull) => pull.agent),
394− issues: openIssues ?? [],
395− }),
396− };
397− const links = projectValue?.links;
398− const checklist = !member
399− ? null
400− : plan.plan !== "production" && plan.plan !== "release"
401− ? startChecklist(plan.plan, {
402− ...firstSteps,
403− hasWorkflow,
404− productionUrl: projectValue?.productionUrl ?? null,
405− hasLinks: !!links && (links.homepage != null || links.docs != null || links.custom.length > 0),
406− hasDocsLink: !!links && (links.docs != null || links.homepage != null),
407− })
408− : isLibrary
409− ? releaseChecklist({
410− ...firstSteps,
411− hasWorkflow,
412− released: hasRelease(published),
413− releaseTo: published[0]
414− ? packagePath(published[0])
415− : (publishGuide(projectValue?.ecosystem ?? null)?.guide ?? "https://docs.g1t.sh/guides/packages/"),
416− })
417− : productionChecklist({
418− ...firstSteps,
419− deploysEnabled: ok(settings)?.enabled ?? false,
420− productionDeployed:
421− liveApps.some((app) => app.kind === "production") ||
422− builds.some((build) => build.kind === "production" && wentLive(build.status)),
423− domains: ok(domains)?.domains.length ?? null,
424− previewOpened:
425− liveApps.some((app) => app.kind === "preview") || builds.some((build) => build.kind === "preview" && wentLive(build.status)),
426− });
427375
428376 // The checks on the latest commit and each active branch's head, in
429377 // one call once the branches are read: streamed in beside them.
442390 commit: ok(log)?.[0] ?? null,
443391 commits: (ok(log) ?? []).map((one) => ({ hash: one.hash, message: one.message.split("\n")[0] ?? "", author: one.author, coAuthors: one.coAuthors, at: one.authoredAt })),
444392 release: latestTag(ok(tagList) ?? []),
445− checklistTitle: plan.title,
446393 // Production as reported from outside g1t, for an app deployed elsewhere.
447394 deployment,
448395 // Its screenshot's address on the site (production-elsewhere.ts), when it has one to show.
472419 knows: knows as Memory[],
473420 memoryCount: memoryList.length,
474421 memoriesLoaded: memories?.ok ?? false,
475− checklist,
476422 library: isLibrary
477423 ? { packages: published.slice(0, PACKAGES_SHOWN), total: published.length, loaded: packageList?.ok ?? false }
478424 : null,
794740 actionData: Route.ComponentProps["actionData"];
795741 params: Route.ComponentProps["params"];
796742 }) {
797− const { member, project, settings, builds, live, commit, open, agentsLive, columns, needs, landed, groups, health, knows, checklist, branches, library, checks } =
743+ const { member, project, settings, builds, live, commit, open, agentsLive, columns, needs, landed, groups, health, knows, branches, library, checks } =
798744 loaderData;
799745 const base = `/${params.owner}/${params.repo}`;
800746 // What it is to its remotes, from the repository layout.
804750 const productionUrl = production ? (settings?.primaryDomain ? `https://${settings.primaryDomain}` : production.url) : null;
805751 const previews = live.filter((app) => app.kind === "preview");
806752 const latestProduction = builds.find((build) => build.kind === "production") ?? null;
807− const plan = project ? checklistPlan(project).plan : "production";
753+ const plan = project ? projectPlan(project) : "production";
808754 const canChange = member && loaderData.can.manage_settings;
809755 // Its homepage and docs beside what it is, unless the card below already links there.
810756 const headUrl =
836782
837783 // A mirror that is not taken over is a read-only copy (lib/mirror.ts
838784 // `offers`): what it is, where it comes from and what it holds, and
839− // nothing to start. Where it runs, the checklist, Right now and the
840− // agents' panels come back with a takeover.
785+ // nothing to start. Where it runs, Right now and the agents' panels
786+ // come back with a takeover.
841787 const readOnlyMirror = mirrorRepo?.mirror && !writable(mirrorRepo.mirror) ? mirrorRepo.mirror : null;
842788 if (readOnlyMirror) {
843789 return (
10781024 </p>
10791025 )}
10801026 </section>
1081−
1082− {checklist && <ProductionChecklist base={base} items={checklist} title={loaderData.checklistTitle} />}
10831027
10841028 <Panel
10851029 title="Right now"
15731517 * The overview of a mirror g1t does not lead (standing by, in CI failover
15741518 * or handing back): the remote it copies and when it last did, the facts
15751519 * of the copy, its latest commits, and About. Nothing here starts
1576− * anything: there is no place it runs, no checklist and no Right now, since
1520+ * anything: there is no place it runs and no Right now, since
15771521 * work happens on the remote until someone takes over.
15781522 */
15791523 function MirrorOverview({
+0−4
6767 title: "Permissions",
6868 about: "Who may do what in the workspace, part by part: repositories, teams, chat, artifacts, agents, deployments and packages. Owners only.",
6969 },
70− "org-chart": {
71− title: "Org chart",
72− about: "Who reports to whom, with each team's agents beside the person who leads it.",
73− },
7470 projects: {
7571 title: "Projects",
7672 about: "Everything the workspace builds and runs. Search, filter and sort them, and pin the ones you use most to keep them in your sidebar.",
+0−136
1−import { Info } from "lucide-react";
2−import { Link, data } from "react-router";
3−
4−import type { Route } from "./+types/org-chart";
5−import { AgentPersonFace, PersonFace, TeamKindMark } from "../../components/people";
6−import { Card } from "../../components/ui/card";
7−import { Hint } from "../../components/ui/hint";
8−import { page } from "../../lib/meta";
9−import { type OrgNode, agentPath, orgChart, peopleAgent, personName, personPath } from "../../lib/people";
10−import { teamPath } from "../../lib/teams";
11−import { identity, workspaceAgents } from "../../lib/services.server";
12−import { getViewer, roleIn, unwrap } from "../../lib/session.server";
13−
14−export function meta({ params, ...args }: Route.MetaArgs) {
15− return page(args, { title: `Org chart · ${params.owner} · g1t` });
16−}
17−
18−export async function loader({ params, context }: Route.LoaderArgs) {
19− const viewer = getViewer(context);
20− if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
21− const [directory, agents] = await Promise.all([
22− identity.peopleDirectory(viewer, params.owner).then(unwrap),
23− workspaceAgents.list(params.owner, viewer!).catch(() => null),
24− ]);
25− return { slug: params.owner.toLowerCase(), directory, agents: agents?.ok ? agents.value.map(peopleAgent) : [] };
26−}
27−
28−export default function OrgChartPage({ loaderData }: Route.ComponentProps) {
29− const { slug, directory, agents } = loaderData;
30− const chart = orgChart(directory, agents);
31− const lines = directory.people.some((person) => person.manager);
32−
33− return (
34− <div className="space-y-6">
35− {!lines && (
36− <Card asChild radius="lg" className="flex items-start gap-2 px-3 py-2.5 text-sm text-muted">
37− <p>
38− <Info size={15} className="mt-0.5 shrink-0 text-faint" />
39− {directory.can_manage
40− ? "No one reports to anyone yet. Set who each person reports to under Edit profile on their profile."
41− : "No reporting lines yet. An owner sets who each person reports to on their profile."}
42− </p>
43− </Card>
44− )}
45− <Card asChild className="overflow-x-auto p-4 sm:p-5">
46− <section aria-label="Reporting lines">
47− <ul className="space-y-1">
48− {chart.roots.map((root) => (
49− <Branch key={root.person.username} slug={slug} node={root} />
50− ))}
51− </ul>
52− <p className="mt-4 text-xs text-faint">The agents on a team appear beside the person who leads it, as its members; each opens the agent's page in Agents.</p>
53− </section>
54− </Card>
55−
56− {chart.unled.length > 0 && (
57− <section aria-labelledby="unled" className="space-y-3">
58− <h2 id="unled" className="text-sm font-medium text-muted">
59− Teams no person leads
60− </h2>
61− <ul className="grid gap-3 sm:grid-cols-2">
62− {chart.unled.map(({ team, agents: teamAgents }) => (
63− <Card asChild key={team.slug} className="p-4">
64− <li>
65− <div className="flex items-center gap-2">
66− <TeamKindMark people={team.people.length} agents={teamAgents.length} />
67− <Link to={teamPath(slug, team.slug)} className="font-medium hover:text-accent">
68− {team.name}
69− </Link>
70− {team.lead?.kind === "agent" && (
71− <span className="text-xs text-faint">led by {teamAgents.find((a) => a.id === (team.lead as { agent_id: string }).agent_id)?.display_name ?? "an agent"}</span>
72− )}
73− </div>
74− <div className="mt-3 flex flex-wrap gap-2">
75− {teamAgents.map((agent) => (
76− <Link key={agent.id} to={agentPath(slug, agent.handle)} className="inline-flex items-center gap-1.5 rounded-full border border-line py-0.5 pr-2.5 pl-0.5 text-xs hover:border-line-strong">
77− <AgentPersonFace agent={agent} size={20} ring="var(--color-surface)" />
78− {agent.display_name}
79− </Link>
80− ))}
81− </div>
82− </li>
83− </Card>
84− ))}
85− </ul>
86− </section>
87− )}
88− </div>
89− );
90−}
91−
92−/** One person, the agents of the teams they lead, and everyone who reports to them below. */
93−function Branch({ slug, node }: { slug: string; node: OrgNode }) {
94− const { person } = node;
95− return (
96− <li>
97− <div className="flex flex-wrap items-center gap-2 py-1">
98− <Link
99− to={personPath(slug, person.username)}
100− prefetch="intent"
101− className="inline-flex min-w-0 items-center gap-2.5 rounded-lg border border-line bg-bg py-1.5 pr-3 pl-1.5 transition-colors hover:border-accent/60"
102− >
103− <PersonFace person={person} size={28} />
104− <span className="grid min-w-0 text-left">
105− <span className="truncate text-sm font-medium">{personName(person)}</span>
106− <span className="truncate text-xs text-muted">{person.title ?? `@${person.username}`}</span>
107− </span>
108− </Link>
109− {node.agents.length > 0 && (
110− <span className="inline-flex items-center gap-2">
111− <span className="flex">
112− {node.agents.slice(0, 6).map((agent, index) => (
113− <Hint key={agent.id} label={`${agent.display_name} · ${agent.title || agent.role} · agent`}>
114− <Link to={agentPath(slug, agent.handle)} className={index ? "-ml-1.5" : undefined} aria-label={agent.display_name}>
115− <AgentPersonFace agent={agent} size={22} ring="var(--color-surface)" />
116− </Link>
117− </Hint>
118− ))}
119− </span>
120− <span className="text-xs text-faint">
121− {node.agents.length > 6 ? `+${node.agents.length - 6} · ` : ""}
122− {node.leads.map((team) => team.name).join(", ")}
123− </span>
124− </span>
125− )}
126− </div>
127− {node.reports.length > 0 && (
128− <ul className="ml-[17px] space-y-1 border-l border-dashed border-line-strong pl-5 sm:pl-7">
129− {node.reports.map((report) => (
130− <Branch key={report.person.username} slug={slug} node={report} />
131− ))}
132− </ul>
133− )}
134− </li>
135− );
136−}
+9−25
55
66 import type { Route } from "./+types/overview";
77 import { DeployLink, host, StatusDot } from "../../components/deploy";
8−import { ButtonLink, CopyLine, TimeAgo } from "../../components/ui";
8+import { ButtonLink, EmptyState, TimeAgo } from "../../components/ui";
99 import { Avatar } from "../../components/ui/avatar";
1010 import { Badge } from "../../components/ui/badge";
1111 import { Card } from "../../components/ui/card";
1212 import { UsageCard } from "../../components/usage-card";
1313 import { PullIcon } from "../../components/work-icons";
1414 import { openedBy } from "../../lib/opened-by";
15−import { cloneUrl, useAddresses } from "../../lib/addresses";
1615 import { kindLabel, libraryPackages, packageLine, packagePath, primaryLink } from "../../lib/project-kind";
1716 import { PinButton } from "../../components/pin-button";
1817 import { sortProjects } from "../../lib/project-list";
118117 }
119118
120119 /** What a member sees in a workspace with no projects yet. */
121−function GetStarted({ slug }: { slug: string }) {
122− const addresses = useAddresses();
120+function NoProjects({ slug }: { slug: string }) {
123121 return (
124− <div className="rounded-2xl border border-line bg-surface p-8">
125− <span className="flex size-10 items-center justify-center rounded-xl bg-accent/10 text-accent ring-1 ring-accent/30">
126− <Box size={18} />
127− </span>
128− <h2 className="mt-4 text-lg font-semibold tracking-tight">Create your first project</h2>
129− <p className="mt-1.5 max-w-xl text-sm text-muted">
130− A project is what you build and run: its code, the issues that say what should change, the agents and
131− people who change it, and where it is deployed.
132− </p>
133− <div className="mt-6 flex flex-wrap gap-3">
134− <ButtonLink to={`/new?workspace=${slug}`} variant="accent">
135− <Plus size={14} />
136− New project
137− </ButtonLink>
138− </div>
139− <p className="mt-6 text-sm text-muted">Or push code you already have, and it becomes a project:</p>
140− <div className="mt-2 max-w-xl">
141− <CopyLine prompt text={`git push ${cloneUrl(addresses, `${slug}/my-project`)} main`} />
142− </div>
143− </div>
122+ <EmptyState title="No projects yet">
123+ <ButtonLink to={`/new?workspace=${slug}`} variant="accent" className="mt-3">
124+ <Plus size={14} />
125+ New project
126+ </ButtonLink>
127+ </EmptyState>
144128 );
145129 }
146130
284268 <div className="grid gap-10 lg:grid-cols-[1fr_18rem]">
285269 <div className="min-w-0 space-y-10">
286270 {total === 0 && role ? (
287− <GetStarted slug={slug} />
271+ <NoProjects slug={slug} />
288272 ) : (
289273 <>
290274 <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
+1−5
1−import { Bot, Network, Search, UserPlus } from "lucide-react";
1+import { Bot, Search, UserPlus } from "lucide-react";
22 import { Link, data, useSearchParams, type ShouldRevalidateFunctionArgs } from "react-router";
33
44 import type { Route } from "./+types/people";
7070 <span role="status" className="mr-1 text-sm text-muted tabular-nums">
7171 {query ? `${entries.length} of ${total}` : total} {total === 1 && !query ? "person" : "people"}
7272 </span>
73− <ButtonLink to={`/${slug}/-/org-chart`} variant="outline">
74− <Network size={15} />
75− Org chart
76− </ButtonLink>
7773 {directory.can_manage && (
7874 <ButtonLink to={`/${slug}/-/members`}>
7975 <UserPlus size={15} />
+6−67
1−import { Bot, ChevronRight, KeyRound, MapPin, MessageSquare, Network, Pencil, UsersRound, Wallet } from "lucide-react";
1+import { Bot, ChevronRight, KeyRound, MapPin, MessageSquare, Pencil, UsersRound, Wallet } from "lucide-react";
22 import { type ReactNode, useEffect, useState } from "react";
33 import { Form, Link, data, redirect, useNavigation } from "react-router";
44
55 import type { DirectoryPerson } from "@g1t/contracts";
66
77 import type { Route } from "./+types/person";
8−import { AgentLink, AgentPersonFace, Coming, LocalTime, PersonFace, PersonLink, TeamKindMark } from "../../../components/people";
8+import { AgentLink, AgentPersonFace, Coming, LocalTime, PersonFace, TeamKindMark } from "../../../components/people";
99 import { PresenceSummary } from "../../../components/presence";
1010 import { ErrorText, Field, Input, SubmitButton, Textarea } from "../../../components/ui";
1111 import { Badge } from "../../../components/ui/badge";
1212 import { Button } from "../../../components/ui/button";
1313 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../../../components/ui/dialog";
14−import { SelectField } from "../../../components/ui/select";
1514 import { channelPath } from "../../../lib/chat";
1615 import { page } from "../../../lib/meta";
1716 import {
1817 agentsOn,
1918 codeAccessWords,
2019 leads,
21− managerChoices,
2220 ownsFromText,
2321 peopleAgent,
2422 personName,
25− reportsOf,
2623 teamsOfPerson,
2724 } from "../../../lib/people";
2825 import { teamPath } from "../../../lib/teams";
3532 return page(args, { title: `${name} · People · ${params.owner} · g1t` });
3633 }
3734
38−/** The few fields of everyone else a profile shows. */
39−function slim(person: DirectoryPerson) {
40− const { user_id, username, display_username, name, avatar, title, manager } = person;
41− return { user_id, username, display_username, name, avatar, title, manager };
42−}
43−
4435 export async function loader({ params, context }: Route.LoaderArgs) {
4536 const viewer = getViewer(context);
4637 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
6051 return {
6152 slug: params.owner.toLowerCase(),
6253 person,
63− people: directory.people.map(slim),
6454 teams,
6555 agents: all,
6656 base: directory.base_permission,
8474 throw redirect(channelPath(params.owner.toLowerCase(), opened.value));
8575 }
8676 if (intent === "profile") {
87− const changes: { title?: string; owns?: string[]; manager?: string } = {
77+ const changes = {
8878 title: String(form.get("title") ?? ""),
8979 owns: ownsFromText(String(form.get("owns") ?? "")),
9080 };
91− if (form.has("manager")) {
92− const manager = String(form.get("manager") ?? "");
93− changes.manager = manager === "none" ? "" : manager;
94− }
9581 const saved = await identity.setMemberProfile(user, params.owner, params.username, changes);
9682 return saved.ok ? { intent, saved: true as const, error: null } : { intent, error: saved.error.message };
9783 }
9985 }
10086
10187 export default function PersonProfile({ loaderData, actionData }: Route.ComponentProps) {
102− const { slug, person, people, teams, agents, base, canManage, self, spend } = loaderData;
103− const manager = person.manager ? people.find((p) => p.username === person.manager) : null;
104− const reports = reportsOf(people as DirectoryPerson[], person.username);
88+ const { slug, person, teams, agents, base, canManage, self, spend } = loaderData;
10589 const teamAgents = teams.flatMap((team) => agentsOn(team, agents).map((agent) => ({ agent, team })));
10690 const seen = new Set<string>();
10791 const uniqueAgents = teamAgents.filter(({ agent }) => (seen.has(agent.id) ? false : (seen.add(agent.id), true)));
154138 </SubmitButton>
155139 </Form>
156140 )}
157− {mayEdit && <EditProfile person={person} people={people as DirectoryPerson[]} canManage={canManage} actionData={actionData} />}
141+ {mayEdit && <EditProfile person={person} actionData={actionData} />}
158142 </div>
159143 </header>
160144 {actionData?.intent === "message" && <ErrorText>{actionData.error}</ErrorText>}
198182 )}
199183 </Card>
200184
201− <Card icon={<Network size={14} />} title="Reporting line">
202− <dl className="space-y-3 text-sm">
203− <div>
204− <dt className="text-xs text-faint">Reports to</dt>
205− <dd className="mt-1">{manager ? <PersonRow slug={slug} person={manager} /> : <Quiet>No one{canManage ? ". Set it under Edit profile." : "."}</Quiet>}</dd>
206− </div>
207− <div>
208− <dt className="text-xs text-faint">
209− Reports <span className="tabular-nums">{reports.length}</span>
210− </dt>
211− <dd className="mt-1 space-y-1.5">
212− {reports.length ? reports.map((report) => <PersonRow key={report.username} slug={slug} person={report} />) : <Quiet>No one reports to them.</Quiet>}
213− </dd>
214− </div>
215− </dl>
216− </Card>
217−
218185 <Card icon={<Bot size={14} />} title="Agents on their teams">
219186 {uniqueAgents.length ? (
220187 <ul className="space-y-2">
285252 return <p className="text-sm text-faint">{children}</p>;
286253 }
287254
288−function PersonRow({ slug, person }: { slug: string; person: Pick<DirectoryPerson, "user_id" | "username" | "display_username" | "name" | "avatar" | "title"> }) {
289− return (
290− <span className="flex min-w-0 items-center gap-2.5">
291− <PersonFace person={person} size={24} ring="var(--color-surface)" />
292− <span className="min-w-0 truncate">
293− <PersonLink workspace={slug} person={person} className="font-medium" />
294− {person.title && <span className="text-muted"> · {person.title}</span>}
295− </span>
296− </span>
297− );
298−}
299−
300−/** Their title and what they own (theirs, or an owner's to change), and who they report to (an owner's). */
255+/** Their title and what they own: theirs, or an owner's, to change. */
301256 function EditProfile({
302257 person,
303− people,
304− canManage,
305258 actionData,
306259 }: {
307260 person: DirectoryPerson;
308− people: DirectoryPerson[];
309− canManage: boolean;
310261 actionData: { intent: string; error: string | null; saved?: true } | undefined;
311262 }) {
312263 const [open, setOpen] = useState(false);
314265 useEffect(() => {
315266 if (saved) setOpen(false);
316267 }, [saved, actionData]);
317− const choices = managerChoices(people, person.username);
318268 return (
319269 <>
320270 <Button variant="outline" onClick={() => setOpen(true)}>
335285 <Field label="What they own" hint="One per line: a product, a process, an account. Agents ask them about these.">
336286 <Textarea name="owns" defaultValue={person.owns.join("\n")} rows={3} placeholder={"storefront\nthe release process"} />
337287 </Field>
338− {canManage && (
339− <label className="block">
340− <span className="mb-1.5 block text-sm font-medium text-muted">Reports to</span>
341− <SelectField
342− name="manager"
343− defaultValue={person.manager ?? "none"}
344− className="w-full"
345− options={[{ value: "none", label: "No one" }, ...choices.map((p) => ({ value: p.username, label: `${personName(p)}${p.title ? ` · ${p.title}` : ""}` }))]}
346− />
347− </label>
348− )}
349288 {actionData?.intent === "profile" && <ErrorText>{actionData.error}</ErrorText>}
350289 <DialogFooter>
351290 <Button variant="outline" type="button" onClick={() => setOpen(false)}>
+5−6
450450 as a member that owners and the team's maintainers add and remove; nothing
451451 on the agent names a team. Every reply and session step, an agent is
452452 told each visible team it is on: who leads it, each person's title, what
453−they own, who they report to, whether they are around and their local
454−time, and who to ask when a person is needed. Ask the person who owns
455−something before guessing. The directory of people is
456−`g1t.sh/<owner>/-/people` (people only; a team's page lists the agents on
457−it), profiles `-/people/<username>`, the org chart `-/org-chart`; an
458−agent's page is `-/agents/<handle>`. Guide:
453+they own, whether they are around and their local time, and who to ask
454+when a person is needed. Ask the person who owns something before
455+guessing. The directory of people is `g1t.sh/<owner>/-/people` (people
456+only; a team's page lists the agents on it), profiles
457+`-/people/<username>`; an agent's page is `-/agents/<handle>`. Guide:
459458 https://docs.g1t.sh/guides/people-and-teams/
460459
461460 Anyone creates an agent by describing it at `g1t.sh/<owner>/-/agents/new`:
+12−55
11 //! People: a workspace's directory of everyone in it, people and agents,
2−//! and what each person's place is: a title, who they report to, and what
3−//! they own. Kept by identity beside membership; agents come from the
2+//! and what each person's place is: a title and what they own. Kept by
3+//! identity beside membership; agents come from the
44 //! agents service, which identity names only by id.
55 //!
66 //! **Profiles.** A member's title and what they own are theirs to write,
7−//! and the workspace's owners can write anyone's. Who someone reports to
8−//! (their manager) is set by owners: always another member, never
9−//! themselves, and never a loop.
7+//! and the workspace's owners can write anyone's.
108 //!
119 //! **Agents on teams.** A team can have agents as well as people: mixed,
1210 //! people only or agents only. An agent is on a team when it was added to
2725 pub const MAX_OWNS: usize = 8;
2826 /// The longest of them.
2927 pub const MAX_OWNS_LENGTH: usize = 60;
30−/// How far up a reporting line is followed, looking for a loop.
31−pub const MAX_CHAIN: usize = 64;
3228
33−/// One member, as the directory, their profile and the org chart show them.
29+/// One member, as the directory and their profile show them.
3430 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3531 pub struct DirectoryPerson {
3632 pub user_id: String,
5046 pub role: Role,
5147 /// Their title in this workspace.
5248 pub title: Option<String>,
53− /// Who they report to, by username.
54− pub manager: Option<String>,
5549 /// What they own: a few short phrases.
5650 pub owns: Vec<String>,
5751 /// When they joined the workspace (RFC 3339).
6559 pub role: TeamRole,
6660 }
6761
68−/// A team as the directory and the org chart need it: who is on it, by
69−/// username and agent id, and who leads it.
62+/// A team as the directory needs it: who is on it, by username and agent
63+/// id, and who leads it.
7064 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
7165 pub struct DirectoryTeam {
7266 pub slug: String,
10296 /// What members get on every repository: `none`, `read`, `write` or `admin`.
10397 pub base_permission: String,
10498 /// Whether the viewer owns the workspace: they can set anyone's
105− /// title, manager and what they own.
99+ /// title and what they own.
106100 pub can_manage: bool,
107101 }
108102
109−/// `set_member_profile`: what is given changes; the rest stays. `manager`
110−/// set to an empty string clears it. Returns `Outcome<DirectoryPerson>`.
103+/// `set_member_profile`: what is given changes; the rest stays. Returns
104+/// `Outcome<DirectoryPerson>`.
111105 #[derive(Debug, Default, Serialize, Deserialize)]
112106 pub struct SetMemberProfileArgs {
113107 pub actor: User,
115109 pub username: String,
116110 #[serde(default)]
117111 pub title: Option<String>,
118− /// A member's username, or an empty string for none. Owners only.
119112 #[serde(default)]
120− pub manager: Option<String>,
121− #[serde(default)]
122113 pub owns: Option<Vec<String>>,
123114 #[serde(default)]
124115 pub surface: Option<crate::audit::Surface>,
151142 out
152143 }
153144
154−/// What may change on someone's profile: their title and what they own
155−/// by themselves or an owner; their manager by an owner.
145+/// What may change on someone's profile: their title and what they own,
146+/// by themselves or an owner.
156147 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
157148 pub enum ProfileField {
158149 Title,
159150 Owns,
160− Manager,
161151 }
162152
163153 /// Whether `actor` may change `field` on someone's profile.
164154 pub fn may_edit_profile(field: ProfileField, is_self: bool, owner: bool) -> bool {
165155 match field {
166156 ProfileField::Title | ProfileField::Owns => is_self || owner,
167− ProfileField::Manager => owner,
168157 }
169158 }
170159
171−/// Whether making `manager` the manager of `person` would make a loop.
172−/// `manager_of` gives each person's current manager.
173−pub fn makes_loop(person: &str, manager: &str, manager_of: impl Fn(&str) -> Option<String>) -> bool {
174− if person == manager {
175− return true;
176− }
177− let mut at = manager.to_owned();
178− for _ in 0..MAX_CHAIN {
179− match manager_of(&at) {
180− Some(next) if next == person => return true,
181− Some(next) => at = next,
182− None => return false,
183− }
184− }
185− // A chain this long is treated as a loop rather than followed further.
186− true
187−}
188−
189160 #[cfg(test)]
190161 mod tests {
191162 use super::*;
192− use std::collections::HashMap;
193163
194164 #[test]
195165 fn titles_are_tidied() {
216186 }
217187
218188 #[test]
219− fn people_write_their_own_and_owners_set_managers() {
189+ fn people_write_their_own_and_owners_anyones() {
220190 assert!(may_edit_profile(ProfileField::Title, true, false));
221191 assert!(may_edit_profile(ProfileField::Owns, false, true));
222192 assert!(!may_edit_profile(ProfileField::Owns, false, false));
223− assert!(!may_edit_profile(ProfileField::Manager, true, false));
224− assert!(may_edit_profile(ProfileField::Manager, false, true));
225− }
226−
227− #[test]
228− fn reporting_lines_never_loop() {
229− // ana -> priya -> chase
230− let managers: HashMap<&str, &str> = HashMap::from([("ana", "priya"), ("priya", "chase")]);
231− let of = |id: &str| managers.get(id).map(|m| (*m).to_owned());
232− assert!(makes_loop("chase", "ana", of));
233− assert!(makes_loop("priya", "priya", of));
234− assert!(!makes_loop("ana", "chase", of));
235− assert!(!makes_loop("dev", "priya", of));
236193 }
237194 }
+0−2
Loading the diff…
+7−11
Loading the diff…
+0−2
Loading the diff…
+4−5
Loading the diff…
+1−3
Loading the diff…
+1−4
Loading the diff…
+2−55
Loading the diff…
+1−5
Loading the diff…