Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
187 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1/**
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)2 * The notify service: live notifications, unread counts, and push to
3 * browsers (Web Push) and to the phone app (through Expo, src/expopush.ts).
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.4 * One feed per person (src/feed.ts).
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)5 *
6 * Reached through service bindings only: `POST /rpc/<method>` with
7 * snake_case bodies (`notifyClient` in @g1t/contracts), and `GET /live`,
8 * the person's feed socket, which the site forwards after checking the
9 * session (NOTIFY_VIEWER_HEADER), with the counts it read (NOTIFY_SEED_HEADER).
10 *
11 * Who sends what:
12 * - chat: `deliver`, for every message (counts for each person in the
13 * conversation, a notification for those it is for) and every read;
14 * - events: `notify`, for every inbox item, and `set_inbox`, the count after
15 * items arrive or are marked, naming the person by username;
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers16 * - the site: `subscribe`, `unsubscribe`, `status`, `set_preferences`,
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)17 * `test`, `presence` and `set_presence`, for the person signed in;
18 * `register_device` and `unregister_device`, for the phone app they
19 * signed in to (through `/-/notify`); and
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.20 * `last_visit` and `mark_visit`, when they were last on a workspace's
21 * Home page (src/visits.ts);
Chat, artifacts and notifications no longer serve g1t's own agents. Direct messages and conversation invites take people only, and a space or folio is shared with people and teams. Chat stops waking agents, drops the RPCs agents posted, read, searched and handed off through, and stops answering buttons on agent cards; old cards keep their links. An agent's old messages still show, under its saved name or as "A deleted agent". Artifacts drops the folio and docs RPCs agents read and edited pages with, and an old agent member of a space can still be removed. Notifications drop "an agent is waiting on you". A deployment no longer trusts a pull request with secrets just because g1t's agent opened it, and the sign-up card no longer promises a team of agents.22 * - the site: `workspace_presence`, how a workspace's people
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.23 * show now (away, in Do Not Disturb, offline, and their statuses);
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers24 * - integrations, later: `set_presence` with a status of `source`
Merge branch 'worktree-agent-a5c94df323642630e'25 * `calendar` or `integration`, for the person they act for;
26 * - events and actions: `repo_changed`, what changed in repositories,
27 * told to whoever watches their pages; and the site `watch_repo`, for a
28 * tab on a repository page whose person it checked may read it.
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers29 *
30 * Presence: each feed works out its person's from their tabs and tells
31 * one room per workspace (src/room.ts), which tells everyone there who is
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.32 * online. See docs.g1t.sh/guides/chat/, "Presence and status".
Merge branch 'worktree-agent-a5c94df323642630e'33 *
34 * Live repository pages: one topic per repository (src/topic.ts) keeps
35 * who is watching it, and hands each change to their feeds. See
36 * docs.g1t.sh/guides/notifications/, "Pages that update live".
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)37 */
38import {
39 NOTIFY_VIEWER_HEADER,
40 identityClient,
41 type FeedDelivery,
42 type ServiceBinding,
43 type Viewer,
44} from "@g1t/contracts";
45
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers46import { FEED_USERNAME_HEADER, FEED_USER_ID_HEADER, Feed, type FeedEnv } from "./feed.ts";
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.47import { onlyPeople } from "./presence.ts";
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.48import { visitWorkspace } from "./visits.ts";
Merge branch 'worktree-agent-a5c94df323642630e'49import { byRepo, cleanRepoId, cleanRepoPath } from "./watch.ts";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)50
51export { Feed } from "./feed.ts";
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers52export { Room } from "./room.ts";
Merge branch 'worktree-agent-a5c94df323642630e'53export { Topic } from "./topic.ts";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)54
55type Env = FeedEnv & {
56 IDENTITY: ServiceBinding;
57 FEEDS: DurableObjectNamespace<Feed>;
58};
59
60function feed(env: Env, userId: string) {
61 return env.FEEDS.get(env.FEEDS.idFromName(userId));
62}
63
64/** A user id from the arguments, or one looked up by username. */
Agents' budget alerts and sessions waiting for approval reach the person they're for: notifications finds who to tell under target as well as at the top level.65async function userIdOf(env: Env, given: { user_id?: unknown; username?: unknown; target?: unknown }): Promise<string | null> {
Chat, artifacts and notifications no longer serve g1t's own agents. Direct messages and conversation invites take people only, and a space or folio is shared with people and teams. Chat stops waking agents, drops the RPCs agents posted, read, searched and handed off through, and stops answering buttons on agent cards; old cards keep their links. An agent's old messages still show, under its saved name or as "A deleted agent". Artifacts drops the folio and docs RPCs agents read and edited pages with, and an old agent member of a space can still be removed. Notifications drop "an agent is waiting on you". A deployment no longer trusts a pull request with secrets just because g1t's agent opened it, and the sign-up card no longer promises a team of agents.66 // Who it's for, at the top level or under `target`.
Agents' budget alerts and sessions waiting for approval reach the person they're for: notifications finds who to tell under target as well as at the top level.67 const target = given.target && typeof given.target === "object" ? (given.target as { user_id?: unknown; username?: unknown }) : null;
68 const args = { user_id: given.user_id ?? target?.user_id, username: given.username ?? target?.username };
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)69 if (typeof args.user_id === "string" && args.user_id) return args.user_id;
70 if (typeof args.username !== "string" || !args.username) return null;
71 const user = await identityClient(env.IDENTITY)
72 .userByUsername(args.username)
73 .catch(() => null);
74 return user?.id ?? null;
75}
76
77/** The most deliveries in one call; chat sends one per person in the conversation. */
78const MAX_DELIVERIES = 1000;
79
80async function answer(env: Env, method: string, args: any): Promise<Response> {
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.81 if (method === "workspace_presence") {
82 // For services: how a workspace's people show now, as its room keeps them.
83 const slug = typeof args?.workspace === "string" ? args.workspace.trim().toLowerCase() : "";
84 if (!slug || !env.ROOMS) return Response.json([]);
85 const everyone = await env.ROOMS.get(env.ROOMS.idFromName(slug)).people();
86 return Response.json(onlyPeople(everyone, Array.isArray(args.user_ids) ? args.user_ids : null));
87 }
Merge branch 'worktree-agent-a5c94df323642630e'88 if (method === "repo_changed") {
89 // For services: each repository's changes to its topic, which tells its watchers.
90 const topics = env.TOPICS;
91 if (!topics) return Response.json({ ok: true });
92 const grouped = byRepo(args?.changes);
93 const results = await Promise.allSettled([...grouped].map(([id, changes]) => topics.get(topics.idFromName(id)).changed(id, changes)));
94 for (const result of results) if (result.status === "rejected") console.error("notify: a repository's change was not told", result.reason);
95 return Response.json({ ok: true });
96 }
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)97 if (method === "deliver") {
98 const items = (Array.isArray(args?.items) ? args.items : []).slice(0, MAX_DELIVERIES) as FeedDelivery[];
99 const byUser = new Map<string, FeedDelivery[]>();
100 for (const item of items) {
101 if (typeof item?.user_id !== "string" || !item.user_id) continue;
102 byUser.set(item.user_id, [...(byUser.get(item.user_id) ?? []), item]);
103 }
104 const results = await Promise.allSettled([...byUser].map(([id, mine]) => feed(env, id).deliver(mine)));
105 for (const result of results) if (result.status === "rejected") console.error("notify: a delivery failed", result.reason);
106 return Response.json({ ok: true });
107 }
108 const userId = await userIdOf(env, args ?? {});
109 if (!userId) return Response.json({ ok: false });
110 const stub = feed(env, userId);
111 switch (method) {
112 case "notify":
113 return Response.json(await stub.notify(args.notification));
114 case "set_inbox":
115 return Response.json(await stub.setInbox(Number(args.unread)));
116 case "subscribe":
117 return Response.json(await stub.subscribe(args.subscription, typeof args.user_agent === "string" ? args.user_agent : null));
118 case "unsubscribe":
119 return Response.json(await stub.unsubscribe(String(args.endpoint ?? "")));
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)120 case "register_device":
121 return Response.json(await stub.registerDevice(args.device));
122 case "unregister_device":
123 return Response.json(await stub.unregisterDevice(String(args.token ?? "")));
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)124 case "status":
125 return Response.json(await stub.status(typeof args.endpoint === "string" ? args.endpoint : null));
126 case "set_preferences":
127 return Response.json(await stub.setPreferences(args.preferences));
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.128 case "last_visit":
129 case "mark_visit": {
130 const workspace = visitWorkspace(args.workspace);
131 if (!workspace) return Response.json({ seen_at: null });
132 return Response.json(method === "last_visit" ? await stub.lastVisit(workspace) : await stub.markVisit(workspace, args.at));
133 }
Merge branch 'worktree-agent-a5c94df323642630e'134 case "watch_repo": {
135 const repoId = cleanRepoId(args.repo_id);
136 const repo = cleanRepoPath(args.repo);
137 if (!repoId || !repo) return Response.json({ ok: false, until: null });
138 return Response.json(await stub.watchRepo(userId, repoId, repo));
139 }
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)140 case "test":
141 return Response.json(await stub.test(typeof args.username === "string" ? args.username : ""));
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers142 case "presence":
143 return Response.json(await stub.own({ user_id: userId, username: typeof args.username === "string" ? args.username : "" }));
144 case "set_presence":
145 return Response.json(
146 await stub.setPresence({ user_id: userId, username: typeof args.username === "string" ? args.username : "" }, args.change ?? {}),
147 );
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)148 default:
149 return new Response("Unknown method\n", { status: 404 });
150 }
151}
152
153/**
154 * `GET /live`, upgraded: the viewer comes in NOTIFY_VIEWER_HEADER, set by
155 * the site after checking the session; trusted only because this Worker
156 * is reachable through service bindings alone.
157 */
158async function live(request: Request, env: Env): Promise<Response> {
159 if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") {
160 return new Response("Expected a WebSocket upgrade\n", { status: 426 });
161 }
162 let viewer: Viewer = null;
163 try {
164 viewer = JSON.parse(request.headers.get(NOTIFY_VIEWER_HEADER) ?? "null") as Viewer;
165 } catch {
166 viewer = null;
167 }
168 if (!viewer?.id) return new Response("Sign in first\n", { status: 401 });
169 const headers = new Headers(request.headers);
170 headers.delete(NOTIFY_VIEWER_HEADER);
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers171 // Who the feed is for, so it can name them to the presence rooms.
172 headers.set(FEED_USER_ID_HEADER, viewer.id);
173 headers.set(FEED_USERNAME_HEADER, viewer.username);
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)174 return feed(env, viewer.id).fetch(new Request(request.url, { method: "GET", headers }));
175}
176
177export default {
178 async fetch(request: Request, env: Env): Promise<Response> {
179 const url = new URL(request.url);
180 if (request.method === "GET" && url.pathname === "/live") return live(request, env);
181 const match = url.pathname.match(/^\/rpc\/([a-z_]+)$/);
182 if (request.method !== "POST" || !match) return new Response("Not found\n", { status: 404 });
183 const args = (await request.json().catch(() => ({}))) as any;
184 return answer(env, match[1], args);
185 },
186} satisfies ExportedHandler<Env>;
187

This file's history is long; its oldest lines are credited to the oldest commit read.