Skip to content
245 linesCodeBlameRaw
1# Contributing to g1t
2
3g1t is built the way it asks others to build: issues and pull requests on
4[g1t.sh/flagon-io/g1t](https://g1t.sh/flagon-io/g1t), required checks that prove
5a change works, and a merge queue that keeps `main` passing.
6
7## A change ships with its docs
8
9Documentation is part of the product, held to the same bar as the code.
10A pull request that changes what someone can do, see or call changes the
11docs in the same pull request:
12
13| If you change | Update |
14| --- | --- |
15| Something a person does on g1t.sh | The guide for it in `apps/docs/src/content/docs/guides/` |
16| An API route, a field, or an MCP tool | `apps/api/src/operations.rs` descriptions (they feed the OpenAPI document and the API reference), the example and notes for it in `apps/api/src/reference.json`, `reference/mcp.md`, and any guide that shows the call. Then refresh the docs' copy of the OpenAPI document with `G1T_WRITE_OPENAPI=1 cargo test -p g1t-api openapi`; `cargo test` fails until you do. |
17| How agents behave | `guides/working-with-g1t.md`, and `apps/web/public/llms.txt` |
18| Settings, limits or prices | The page that names them, and the table it is in |
19| A new feature | A section in the guide that owns it, linked from the docs home if it is a new task |
20| How g1t is put together: a service, a binding, a store, an event route, the path of a request | Its page in the docs' Architecture section (`apps/docs/src/content/docs/architecture/`), and its diagram in `apps/docs/src/diagrams/architecture.ts`. [How g1t ships](https://docs.g1t.sh/architecture/shipping/#keeping-this-section-true) says which page covers what. |
21
22The style, in short: plain sentences, second person, no marketing words,
23sentence-case headings, a numbered list for steps, a table for options, and
24every API mention with its exact route or tool name. Examples are copyable
25and real. Nothing is documented that the code does not do.
26
27Check the docs build before you push:
28
29```sh
30cd apps/docs && npm run build
31```
32
33## Icons
34
35- **Interface icons come from [Lucide](https://lucide.dev/icons).** In the
36 apps, import them from `lucide-react`. In the docs, use `@lucide/astro`:
37 import an icon component, or give `apps/docs/src/components/Card.astro`
38 and `Aside.astro` a Lucide name such as `icon="git-branch"`. Starlight's
39 own `<Card>`, `<LinkCard>` and `<Aside>` draw Starlight's icon set, so the
40 docs import the wrappers in `apps/docs/src/components/` instead.
41- **Brand marks come from [Simple Icons](https://simpleicons.org)**
42 (`simple-icons`), and follow each brand's own usage guidelines. Add the
43 dependency with the first one you show.
44- **The g1t logo and illustrations are our own artwork**, in
45 `apps/web/app/components/logo.tsx` and `art.tsx`.
46- Never hand-copy an icon's SVG paths into a file. If Lucide lacks the
47 icon you need, pick the closest one that it has.
48
49## Interface components
50
51- **Build the site from the components in `apps/web/app/components/ui`**,
52 which are [shadcn/ui](https://ui.shadcn.com) components themed with
53 g1t's tokens: buttons, cards, badges, tables, tabs, separators,
54 breadcrumbs, menus, dialogs, sheets, tooltips, form controls.
55- **If shadcn has a component you need and `ui` doesn't, add it there**
56 first, themed with the tokens, then use it. Don't rebuild it inline.
57- **Change how something looks through the component's variants**, not
58 with a one-off class string at the call site. A long `className` on a
59 `div` that draws a card, a pill or a button is a component that should
60 exist.
61- Hover hints use the `Tooltip` component, never `title=`. Native
62 `<select>`, file inputs, checkboxes and radios use their `ui`
63 components.
64- **Every avatar is `Avatar`** (`ui/avatar.tsx`), and every agent's is
65 `Avatar` with `agent` set, or `AgentAvatar` (`components/agent-avatar.tsx`),
66 which wraps it. That draws the agent's bot face (`AgentFace`,
67 `components/agent-face.tsx`) from its `look` or its `avatar_seed`, puts
68 the agent marker in its corner and names it "…, agent" to a screen
69 reader. Never hand-draw an agent's face, a sparkle, a letter on a tile or
70 the marker at a call site: a place that shows an agent differently is a
71 bug.
72- Look at every page you change, in light and dark, on a desktop and a
73 phone, before calling it done.
74
75## Before you push
76
77- `cargo test` in the crate or service you changed.
78- `npx tsc -b --force` in `apps/web` (the incremental build misses changes
79 in `packages/contracts`).
80- Look at what you changed in a browser. Screenshots catch what type
81 checks do not.
82
83## Checking g1t against GitHub
84
85g1t runs GitHub Actions workflows as GitHub does. The Conformance
86workflow is how that is checked: `.g1t/workflows/conformance.yml` and
87`.github/workflows/conformance.yml` are the same file (a test in
88`crates/actions` fails when they differ), and each run's
89`conformance-report` artifact records what its jobs saw: which `GITHUB_*`
90and `RUNNER_*` variables were set and of what kind, every key of the
91contexts and the event and its type, how the workspace is laid out, and
92the values of outputs, files, caches, artifacts and expressions that
93should be the same everywhere. It reads no secrets and runs only by hand.
94
951. Run it on both, at the same commit: **Actions → Conformance → Run
96 workflow** on GitHub and on g1t.sh.
972. Download each run's `conformance-report` artifact, and name the two
98 files `github.json` and `g1t.json`.
993. Compare them:
100
101 ```sh
102 node scripts/conformance-diff.mjs github.json g1t.json
103 ```
104
105It lists what GitHub has that g1t lacks or has otherwise, then what only
106g1t has, and exits 1 while there are differences. A difference is a bug in
107g1t, or belongs in [What g1t can't do yet](https://docs.g1t.sh/about/limitations/).
108Add a step and a `CHECK_` value to the workflow for anything new g1t
109should match.
110
111## Deploying
112
113Pushes to `main` deploy themselves: `.g1t/workflows/deploy.yml` runs
114`scripts/deploy.mjs`, which deploys only the parts that changed, migrations
115first. Every deployable part is listed in `deploy/stack.jsonc`; a new service
116or app goes there, in the table on
117[How a self-hosted g1t runs](https://docs.g1t.sh/guides/self-hosting-architecture/#each-part),
118and in the one on [Services and contracts](https://docs.g1t.sh/architecture/services/#every-unit)
119(`npm run test:deploy` says what is missing).
120[Deploy g1t to Cloudflare](https://docs.g1t.sh/guides/deploy-to-cloudflare/)
121covers the tool, the workflow, rollbacks, adding a unit and first-time
122setup.
123
124- **Migrations run before the code**, so the old code reads the new schema
125 for a minute or more. Add tables and columns; change what rows mean in
126 two deploys (code that reads both forms first); never drop what live code
127 still reads.
128- **The self-hosted runner** is released, not deployed: bump `version` in
129 `crates/runner/Cargo.toml`, merge, and push a `runner-v<version>` tag.
130 `.g1t/workflows/runner-release.yml` builds, signs and publishes it, and
131 runners update themselves to it. `scripts/runner-release.mjs` does the
132 same by hand.
133- **The g1t CLI** is released the same way: bump `version` in
134 `crates/g1t/Cargo.toml`, merge, and push a `cli-v<version>` tag.
135 `.g1t/workflows/cli-release.yml` builds it for every platform, signs
136 the release with the runner's release key and publishes it to
137 g1t.sh/downloads/cli/, where g1t.sh/install and g1t.sh/install.ps1
138 find it. `scripts/cli-release.mjs` does the same by hand. The command
139 reference in the docs is generated from the CLI
140 (`G1T_WRITE_DOCS=1 cargo test -p g1t cli_reference`), and its table of
141 API operations from the OpenAPI document
142 (`G1T_WRITE_OPS=1 cargo test -p g1t ops_table`); tests fail when either
143 falls behind. The guide is [The g1t CLI](https://docs.g1t.sh/guides/cli/).
144- **The desktop app** is released the same way: bump `version` in
145 `apps/desktop/src-tauri/Cargo.toml`, merge, and push a
146 `desktop-v<version>` tag. `.g1t/workflows/desktop-release.yml` builds
147 the installers (Linux on g1t's machines; Windows and macOS on machines
148 registered for them, or Windows cross-built on Linux once its download
149 hosts are allowed, each switched on by a repository variable the
150 workflow's header names; a platform with no machine is left out of that
151 release), signs the updater's files and publishes them, and installed
152 apps update themselves. `scripts/desktop-release.mjs` does the
153 same by hand. The app loads the live site, so a change to the site needs
154 no release; only a change under `apps/desktop` does. The guide is
155 [The desktop app](https://docs.g1t.sh/guides/desktop/).
156- **The phone app** for Android is released on g1t.sh/download while it
157 is not in Google Play (that waits on Flagon's D-U-N-S number): bump
158 `version` in `apps/mobile/app.config.ts` when its native side changes,
159 merge, and push a `mobile-v<version>` tag.
160 `.g1t/workflows/mobile-release.yml` has EAS build the APK and publishes
161 it; `scripts/mobile-release.mjs` does the same by hand. A change to the
162 app's code alone needs no release: `eas update --channel preview` in
163 `apps/mobile` reaches installed copies. Its server half (chat scopes,
164 `/-/notify`, notify's phone push) deploys with the site and services.
165 The guide is [The phone app](https://docs.g1t.sh/guides/mobile/).
166
167## Speed
168
169Pages are a few rounds of service calls, and each round costs a trip to
170the databases, so start calls together and add no rounds.
171
172- Every page answers with `Server-Timing`: each loader, each service's
173 calls and their database time. Look at it in DevTools when a page feels
174 slow.
175- `scripts/perf/measure.ps1` times pages from your machine (`-BrowserUA`
176 for signed-out pages as a browser sees them). A page that only reads
177 must not set the `g1t_d1` cookie: a new read-only service method goes in
178 `READS` in `apps/web/app/lib/perf.ts`.
179- Targets from the US: signed-out pages under 200 ms to first byte,
180 signed-in pages under 400 ms, streamed panels within a second.
181- Workers run without Smart Placement; `scripts/perf/placement-probe.mjs`
182 measures a placement before you pin one.
183- A TypeScript service reports its database time too (`db;dur`, from
184 `openD1` in `@g1t/contracts`): the round trips a method makes are what
185 it costs, so start them together (`Promise.all`), keep what never
186 changes across requests in the isolate (the artifacts service keeps a
187 workspace's slug for 30 s and whether its General space exists), and
188 don't wake a Durable Object for a read D1 can answer.
189- Nothing live gates a page. A page carries what it shows (a doc's text
190 and its saved document), opens its sockets as it hydrates rather than
191 from inside the code they feed, keeps what is typed until they answer,
192 and says so with a dot, never a spinner. A page opened with an access
193 token gets its socket tickets with the page (the root loader mints the
194 feed's, an artifact's loader its room's) rather than asking for each.
195- Code that only the browser runs (the doc editor) is still fetched with
196 the page: `vite.config.ts` notes the chunk and what it imports for the
197 route's `links`, and a link to such a page warms the chunk on hover.
198 Modules several routes share are grouped by who shares them
199 (`codeSplitting.groups`), so a page fetches tens of files, not a
200 hundred and more. Before this, opening a doc fetched 124 files
201 (1,049 KB) in three waves and the editor appeared at 2.8–3.3 s; a doc
202 now fetches 71 files (1,026 KB) in one wave, and against the deployed
203 services the editor appears at 2.1 s, of which the room's answer is
204 0.65 s and the editor's own start 0.4 s. With the saved document in the
205 page, measured on g1t.sh signed in (2026-10-10, warm, 1440×900), the
206 editor appears at 1.5 s: first byte 0.53 s, scripts by 0.75 s, the
207 editor's own start the rest. A doc without a saved document (one last
208 saved before documents were kept) waited for its room's first message
209 instead, 0.9 s after the socket opened, and appeared at 2.0–2.5 s; the
210 page now asks the room once and keeps the answer, so only its first
211 open pays. Still to cut: the live socket's four service calls before
212 the room answers, the page and sidebar's 27 database round trips in two
213 calls, the root loader's billing and identity calls (0.7–1.3 s and
214 0.45 s inside), and the 3.4 MB of script a doc page decodes.
215
216## Rate limits
217
218`RATE_LIMITS` in `packages/contracts/src/rate-limits.ts` is the table of
219record, and the [rate limits page](https://docs.g1t.sh/reference/rate-limits/)
220is the public one: change both, and the binding in the Worker's
221`wrangler.jsonc`, together (`front-door-limits.test.ts` fails when they
222disagree). Limits fail open, keys hash anything secret, and each Worker
223takes its namespace ids from its own block of a hundred (41xx packages,
22442xx web, 43xx repos, 44xx api, 45xx og, 46xx status).
225
226## Operating g1t.sh
227
228For Flagon staff.
229
230- **Incidents** are declared, updated and resolved in sudo, under
231 **Platform → Incidents**, and appear on status.g1t.sh within 30 seconds.
232 Declare as soon as people are affected, and pick the higher severity
233 when unsure. SEV1 (down for most people, or data at risk) gets an update
234 at least every 30 minutes and SEV2 (a core part broken for many) at least
235 hourly; both email subscribers and need a blameless postmortem within
236 five working days.
237- **An Artifacts outage:** `scripts/ops/gitstore-namespaces.mjs` shows a
238 failing namespace. After 15 minutes, serve it read-only from the fallback
239 git store (`scripts/ops/restore-to-gitstore.mjs restore`, then the repos
240 Worker's secret `GIT_FALLBACK_NAMESPACES`) and open an incident. To
241 switch back, `reconcile` anything the fallback took, then delete the
242 secret. The script's header has every step.
243- **Costs and margin**, model prices, credits and the platform pause are
244 in sudo, under **Costs & margin**. The code is in `services/billing/src`
245 (`costs.rs`, `margin.rs`, `pricing.rs`, `budget.rs`, `platform.rs`).