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
122 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 site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today1/**
2 * What product analytics may learn from a page, decided before an event
3 * leaves the browser (lib/analytics.client.ts).
4 *
5 * The public front door (the home page, pricing, Explore, signing up and
6 * in, support, security and the policies) is sent as it is. Everywhere else, names are replaced
7 * with placeholders: `/acme/web/pull/12` is sent as
8 * `/:name/:name/pull/:n`, the page title as "g1t", and a clicked element
9 * without its text, link or attributes. Query strings keep only `utm_*`.
10 * Session recordings and error reports are never sent, and click heatmaps
11 * only from the front door. No Workers or browser imports, so it is tested
12 * under Node.
13 */
14
15/** Paths sent as they are. */
16const PUBLIC = new Set([
17 "/",
18 "/pricing",
19 "/explore",
20 "/register",
21 "/login",
22 "/security",
23 "/support",
24 "/status",
25 "/policies",
26]);
27
28/**
29 * Words of g1t's own addresses, kept in a scrubbed path so it still says
30 * which kind of page it was. Anything else is a name, and is replaced.
31 */
32const ROUTE_WORDS = new Set([
33 "-", "about.json", "account", "actions", "activity", "agents", "applications", "archive", "audit", "billing",
34 "blob", "branches", "browse", "bypass-requests", "chat", "checks", "code", "code-access", "commit", "commits",
35 "compare", "confirm-email", "context", "deployments", "dm", "docs", "emails", "emoji", "entries", "explore", "files", "forgot", "gateway",
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.36 "github", "guardrails", "home", "inbox", "apps", "insights", "integrations", "invitations", "invite", "invites", "issues",
Memory is gone: people's own harnesses keep their own memory (CLAUDE.md, AGENTS.md, auto-memory), so g1t's project and workspace notes that agents recalled and remembered only duplicated them. The recall and remember operations, their /repos/{owner}/{name}/memory routes, the memory MCP tool and its lines in the MCP instructions, the Memory section of the API reference and OpenAPI document (and the CLI's table generated from it) are removed, as are the work service's memory methods, the Memory pages of a project and of a workspace with their sidebar links, Home's Decisions card (which read decisions out of workspace memory), the Memory guide (its address and the old agent-memory, agents-and-memory and context-hub ones now lead to Connect a coding agent) and every mention in the docs, README, llms.txt, the landing page, the privacy policy and the Ask bench. Decision records, g1t why and g1t work stay as they were. memory:read and memory:write are retired as agents:run and the models scopes were: tokens and OAuth grants that hold them still parse and keep working for their other scopes, but no preset (the Agent preset and the OAuth default included), token form, mobile sign-in or hosted session hands them out and nothing needs them; looking up a ticket (search ticket, get_context), which needed memory:read, needs issues:read. The memories table stays in work's D1 untouched and is no longer read or written, except that purging a repository or deleting a workspace still deletes its old rows. Checked: cargo test for g1t-contracts (308), g1t-api (201), g1t (102 and 41), g1t-work (103) and g1t-integrations (65), cargo check of the workspace, the OpenAPI document and ops table regenerated, every workspace's typecheck, the site's 901 tests and a production build, the contracts' 48 tests, the phone app's 11, the deploy tool's 92, and the docs' tests and build.37 "jobs", "keys", "labels", "login", "members", "merge-queue", "milestones", "new", "notifications", "overview",
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today38 "packages", "patterns", "people", "personal-access-tokens", "pins", "policies", "profile", "projects", "pull",
39 "pulls", "queue", "releases", "repositories", "reset", "rules", "runners", "runs", "search", "secrets", "security",
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.40 "security-log", "settings", "soon", "spend", "tags", "teams", "today", "tokens", "tree", "two-factor", "u", "usage",
Merge branch 'worktree-agent-aa68cf6ecc03dffaf'41 "verify", "webhooks", "work", "workspace", "workspaces",
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today42]);
43
44export function isPublicPath(pathname: string): boolean {
45 const path = pathname.replace(/\/+$/, "") || "/";
46 return PUBLIC.has(path) || path.startsWith("/policies/");
47}
48
49/** A path as analytics may see it. */
50export function scrubPath(pathname: string): string {
51 if (isPublicPath(pathname)) return pathname;
52 return pathname
53 .split("/")
54 .map((segment) => {
55 if (segment === "" || ROUTE_WORDS.has(segment)) return segment;
56 return /^\d+$/.test(segment) ? ":n" : ":name";
57 })
58 .join("/");
59}
60
61/** Only campaign parameters survive; everything else in a query can name something. */
62function scrubSearch(search: URLSearchParams): string {
63 const kept = new URLSearchParams();
64 for (const [key, value] of search) if (key.startsWith("utm_")) kept.append(key, value);
65 const query = kept.toString();
66 return query ? `?${query}` : "";
67}
68
69/** A URL on this site as analytics may see it; another site's is left alone. */
70export function scrubUrl(value: string, origin: string): string {
71 let url: URL;
72 try {
73 url = new URL(value);
74 } catch {
75 return value;
76 }
77 if (url.origin !== origin) return value;
78 return `${url.origin}${scrubPath(url.pathname)}${scrubSearch(url.searchParams)}`;
79}
80
81/** Element text, links and attributes in autocapture's chain string. */
82const CHAIN_DETAIL = /(?:text|href|attr__[\w-]+)="(?:[^"\\]|\\.)*"/g;
83
84function scrubValue(key: string, value: unknown, origin: string, publicPage: boolean): unknown {
85 if (typeof value === "string") {
86 if (!publicPage && (key === "$title" || key === "title")) return "g1t";
87 if (!publicPage && key === "$el_text") return undefined;
88 if (!publicPage && key === "$elements_chain") return value.replace(CHAIN_DETAIL, "");
89 if (/pathname$/i.test(key) && value.startsWith("/")) return scrubPath(value);
90 return value.startsWith(origin) ? scrubUrl(value, origin) : value;
91 }
92 if (Array.isArray(value)) return value.map((item) => scrubValue(key, item, origin, publicPage));
93 if (value && typeof value === "object") {
94 const out: Record<string, unknown> = {};
95 for (const [inner, innerValue] of Object.entries(value)) {
96 if (!publicPage && (inner === "$el_text" || inner === "href" || inner.startsWith("attr__"))) continue;
97 const scrubbed = scrubValue(inner, innerValue, origin, publicPage);
98 // Some objects are keyed by address, such as a heatmap's pages.
99 if (scrubbed !== undefined) out[inner.startsWith(origin) ? scrubUrl(inner, origin) : inner] = scrubbed;
100 }
101 return out;
102 }
103 return value;
104}
105
106/** Events that are never sent: session recordings and error reports, which carry page content. */
107const NEVER = new Set(["$snapshot", "$snapshot_items", "$exception"]);
108
109export type CapturedEvent = { event: string; properties: Record<string, unknown>; [key: string]: unknown };
110
111/**
112 * The event as analytics may see it, or null to drop it. `pathname` is the
113 * page the event happened on; `origin` is this site's.
114 */
115export function scrubEvent<T extends CapturedEvent>(event: T | null, pathname: string, origin: string): T | null {
116 if (!event || NEVER.has(event.event)) return null;
117 const publicPage = isPublicPath(pathname);
118 // A heatmap is keyed by the page's address, and holds where on it people clicked.
119 if (event.event === "$$heatmap" && !publicPage) return null;
120 const properties = scrubValue("", event.properties ?? {}, origin, publicPage) as Record<string, unknown>;
121 return { ...event, properties };
122}

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