| 1 | # Contributing to g1t |
| 2 | |
| 3 | g1t 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 |
| 5 | a change works, and a merge queue that keeps `main` passing. |
| 6 | |
| 7 | ## A change ships with its docs |
| 8 | |
| 9 | Documentation is part of the product, held to the same bar as the code. |
| 10 | A pull request that changes what someone can do, see or call changes the |
| 11 | docs 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 | |
| 21 | The style, in short: plain sentences, second person, no marketing words, |
| 22 | sentence-case headings, a numbered list for steps, a table for options, and |
| 23 | every API mention with its exact route or tool name. Examples are copyable |
| 24 | and real. Nothing is documented that the code does not do. |
| 25 | |
| 26 | Check the docs build before you push: |
| 27 | |
| 28 | ```sh |
| 29 | cd apps/docs && npm run build |
| 30 | ``` |
| 31 | |
| 32 | ## Icons |
| 33 | |
| 34 | - **Interface icons come from [Lucide](https://lucide.dev/icons).** In the |
| 35 | apps, import them from `lucide-react`. In the docs, use `@lucide/astro`: |
| 36 | import an icon component, or give `apps/docs/src/components/Card.astro` |
| 37 | and `Aside.astro` a Lucide name such as `icon="git-branch"`. Starlight's |
| 38 | own `<Card>`, `<LinkCard>` and `<Aside>` draw Starlight's icon set, so the |
| 39 | docs import the wrappers in `apps/docs/src/components/` instead. |
| 40 | - **Brand marks come from [Simple Icons](https://simpleicons.org)** |
| 41 | (`simple-icons`), and follow each brand's own usage guidelines. Add the |
| 42 | dependency with the first one you show. |
| 43 | - **The g1t logo and illustrations are our own artwork**, in |
| 44 | `apps/web/app/components/logo.tsx` and `art.tsx`. |
| 45 | - Never hand-copy an icon's SVG paths into a file. If Lucide lacks the |
| 46 | icon you need, pick the closest one that it has. |
| 47 | |
| 48 | ## Interface components |
| 49 | |
| 50 | - **Build the site from the components in `apps/web/app/components/ui`**, |
| 51 | which are [shadcn/ui](https://ui.shadcn.com) components themed with |
| 52 | g1t's tokens: buttons, cards, badges, tables, tabs, separators, |
| 53 | breadcrumbs, menus, dialogs, sheets, tooltips, form controls. |
| 54 | - **If shadcn has a component you need and `ui` doesn't, add it there** |
| 55 | first, themed with the tokens, then use it. Don't rebuild it inline. |
| 56 | - **Change how something looks through the component's variants**, not |
| 57 | with a one-off class string at the call site. A long `className` on a |
| 58 | `div` that draws a card, a pill or a button is a component that should |
| 59 | exist. |
| 60 | - Hover hints use the `Tooltip` component, never `title=`. Native |
| 61 | `<select>`, file inputs, checkboxes and radios use their `ui` |
| 62 | components. |
| 63 | - **Every avatar is `Avatar`** (`ui/avatar.tsx`), and every agent's is |
| 64 | `Avatar` with `agent` set, or `AgentAvatar` (`components/agent-avatar.tsx`), |
| 65 | which wraps it. That draws the agent's bot face (`AgentFace`, |
| 66 | `components/agent-face.tsx`) from its `look` or its `avatar_seed`, puts |
| 67 | the agent marker in its corner and names it "…, agent" to a screen |
| 68 | reader. Never hand-draw an agent's face, a sparkle, a letter on a tile or |
| 69 | the marker at a call site: a place that shows an agent differently is a |
| 70 | bug. |
| 71 | - Look at every page you change, in light and dark, on a desktop and a |
| 72 | phone, before calling it done. |
| 73 | |
| 74 | ## Before you push |
| 75 | |
| 76 | - `cargo test` in the crate or service you changed. |
| 77 | - `npx tsc -b --force` in `apps/web` (the incremental build misses changes |
| 78 | in `packages/contracts`). |
| 79 | - Look at what you changed in a browser. Screenshots catch what type |
| 80 | checks do not. |
| 81 | |
| 82 | ## Deploying |
| 83 | |
| 84 | Pushes to `main` deploy themselves: `.g1t/workflows/deploy.yml` runs |
| 85 | `scripts/deploy.mjs`, which deploys only the parts that changed, migrations |
| 86 | first. Every deployable part is listed in `deploy/stack.jsonc`; a new service |
| 87 | or app goes there, and in the table on |
| 88 | [How a self-hosted g1t runs](https://docs.g1t.sh/guides/self-hosting-architecture/#each-part) |
| 89 | (`npm run test:deploy` says what is missing). |
| 90 | [Deploy g1t to Cloudflare](https://docs.g1t.sh/guides/deploy-to-cloudflare/) |
| 91 | covers the tool, the workflow, rollbacks, adding a unit and first-time |
| 92 | setup. |
| 93 | |
| 94 | - **Migrations run before the code**, so the old code reads the new schema |
| 95 | for a minute or more. Add tables and columns; change what rows mean in |
| 96 | two deploys (code that reads both forms first); never drop what live code |
| 97 | still reads. |
| 98 | - **The self-hosted runner** is released, not deployed: bump `version` in |
| 99 | `crates/runner/Cargo.toml`, merge, and push a `runner-v<version>` tag. |
| 100 | `.g1t/workflows/runner-release.yml` builds, signs and publishes it, and |
| 101 | runners update themselves to it. `scripts/runner-release.mjs` does the |
| 102 | same by hand. |
| 103 | - **The desktop app** is released the same way: bump `version` in |
| 104 | `apps/desktop/src-tauri/Cargo.toml`, merge, and push a |
| 105 | `desktop-v<version>` tag. `.g1t/workflows/desktop-release.yml` builds |
| 106 | the installers (Windows and Linux on Linux; macOS on a Mac runner when |
| 107 | one is registered), signs the updater's files and publishes them, and |
| 108 | installed apps update themselves. `scripts/desktop-release.mjs` does the |
| 109 | same by hand. The app loads the live site, so a change to the site needs |
| 110 | no release; only a change under `apps/desktop` does. The guide is |
| 111 | [The desktop app](https://docs.g1t.sh/guides/desktop/). |
| 112 | |
| 113 | ## Speed |
| 114 | |
| 115 | Pages are a few rounds of service calls, and each round costs a trip to |
| 116 | the databases, so start calls together and add no rounds. |
| 117 | |
| 118 | - Every page answers with `Server-Timing`: each loader, each service's |
| 119 | calls and their database time. Look at it in DevTools when a page feels |
| 120 | slow. |
| 121 | - `scripts/perf/measure.ps1` times pages from your machine (`-BrowserUA` |
| 122 | for signed-out pages as a browser sees them). A page that only reads |
| 123 | must not set the `g1t_d1` cookie: a new read-only service method goes in |
| 124 | `READS` in `apps/web/app/lib/perf.ts`. |
| 125 | - Targets from the US: signed-out pages under 200 ms to first byte, |
| 126 | signed-in pages under 400 ms, streamed panels within a second. |
| 127 | - Workers run without Smart Placement; `scripts/perf/placement-probe.mjs` |
| 128 | measures a placement before you pin one. |
| 129 | - A TypeScript service reports its database time too (`db;dur`, from |
| 130 | `openD1` in `@g1t/contracts`): the round trips a method makes are what |
| 131 | it costs, so start them together (`Promise.all`), keep what never |
| 132 | changes across requests in the isolate (the artifacts service keeps a |
| 133 | workspace's slug for 30 s and whether its General space exists), and |
| 134 | don't wake a Durable Object for a read D1 can answer. |
| 135 | - Nothing live gates a page. A page carries what it shows (a doc's text |
| 136 | and its saved document), opens its sockets as it hydrates rather than |
| 137 | from inside the code they feed, keeps what is typed until they answer, |
| 138 | and says so with a dot, never a spinner. A page opened with an access |
| 139 | token gets its socket tickets with the page (the root loader mints the |
| 140 | feed's, an artifact's loader its room's) rather than asking for each. |
| 141 | - Code that only the browser runs (the doc editor) is still fetched with |
| 142 | the page: `vite.config.ts` notes the chunk and what it imports for the |
| 143 | route's `links`, and a link to such a page warms the chunk on hover. |
| 144 | Modules several routes share are grouped by who shares them |
| 145 | (`codeSplitting.groups`), so a page fetches tens of files, not a |
| 146 | hundred and more. Before this, opening a doc fetched 124 files |
| 147 | (1,049 KB) in three waves and the editor appeared at 2.8–3.3 s; a doc |
| 148 | now fetches 71 files (1,026 KB) in one wave, and against the deployed |
| 149 | services the editor appears at 2.1 s, of which the room's answer is |
| 150 | 0.65 s and the editor's own start 0.4 s. With the saved document in the |
| 151 | page, measured on g1t.sh signed in (2026-10-10, warm, 1440×900), the |
| 152 | editor appears at 1.5 s: first byte 0.53 s, scripts by 0.75 s, the |
| 153 | editor's own start the rest. A doc without a saved document (one last |
| 154 | saved before documents were kept) waited for its room's first message |
| 155 | instead, 0.9 s after the socket opened, and appeared at 2.0–2.5 s; the |
| 156 | page now asks the room once and keeps the answer, so only its first |
| 157 | open pays. Still to cut: the live socket's four service calls before |
| 158 | the room answers, the page and sidebar's 27 database round trips in two |
| 159 | calls, the root loader's billing and identity calls (0.7–1.3 s and |
| 160 | 0.45 s inside), and the 3.4 MB of script a doc page decodes. |
| 161 | |
| 162 | ## Rate limits |
| 163 | |
| 164 | `RATE_LIMITS` in `packages/contracts/src/rate-limits.ts` is the table of |
| 165 | record, and the [rate limits page](https://docs.g1t.sh/reference/rate-limits/) |
| 166 | is the public one: change both, and the binding in the Worker's |
| 167 | `wrangler.jsonc`, together (`front-door-limits.test.ts` fails when they |
| 168 | disagree). Limits fail open, keys hash anything secret, and each Worker |
| 169 | takes its namespace ids from its own block of a hundred (41xx packages, |
| 170 | 42xx web, 43xx repos, 44xx api, 45xx og, 46xx status). |
| 171 | |
| 172 | ## Operating g1t.sh |
| 173 | |
| 174 | For Flagon staff. |
| 175 | |
| 176 | - **Incidents** are declared, updated and resolved in sudo, under |
| 177 | **Platform → Incidents**, and appear on status.g1t.sh within 30 seconds. |
| 178 | Declare as soon as people are affected, and pick the higher severity |
| 179 | when unsure. SEV1 (down for most people, or data at risk) gets an update |
| 180 | at least every 30 minutes and SEV2 (a core part broken for many) at least |
| 181 | hourly; both email subscribers and need a blameless postmortem within |
| 182 | five working days. |
| 183 | - **An Artifacts outage:** `scripts/ops/gitstore-namespaces.mjs` shows a |
| 184 | failing namespace. After 15 minutes, serve it read-only from the fallback |
| 185 | git store (`scripts/ops/restore-to-gitstore.mjs restore`, then the repos |
| 186 | Worker's secret `GIT_FALLBACK_NAMESPACES`) and open an incident. To |
| 187 | switch back, `reconcile` anything the fallback took, then delete the |
| 188 | secret. The script's header has every step. |
| 189 | - **Costs and margin**, model prices, credits and the platform pause are |
| 190 | in sudo, under **Costs & margin**. The code is in `services/billing/src` |
| 191 | (`costs.rs`, `margin.rs`, `pricing.rs`, `budget.rs`, `platform.rs`). |