Merge branch 'main' into worktree-agent-a654d8ba46392bedd
55 files+1509−4160/55 viewed
| 7 | 7 | "name": "g1t", | |
| 8 | 8 | "source": "./plugins/claude-code", | |
| 9 | 9 | "description": "Work on g1t issues from Claude Code: records your sessions on g1t, loads an issue's work context, claims and hands off work, and connects g1t's MCP server.", | |
| 10 | − | "version": "0.2.4", | |
| 10 | + | "version": "0.2.5", | |
| 11 | 11 | "category": "development" | |
| 12 | 12 | } | |
| 13 | 13 | ] |
| 41 | 41 | integrations: [ | |
| 42 | 42 | starlight({ | |
| 43 | 43 | title: 'g1t docs', | |
| 44 | − | description: 'Guides and reference for g1t, where a team and its coding agents talk, build and ship.', | |
| 44 | + | description: 'Guides and reference for g1t, the code platform for your team and its coding agents.', | |
| 45 | 45 | components: { | |
| 46 | 46 | Footer: './src/components/Footer.astro', | |
| 47 | 47 | Head: './src/components/Head.astro', |
| 1 | + | User-agent: * | |
| 2 | + | Allow: / | |
| 3 | + | ||
| 4 | + | Sitemap: https://docs.g1t.sh/sitemap-index.xml |
| 1 | 1 | { | |
| 2 | 2 | "name": "g1t docs", | |
| 3 | 3 | "short_name": "g1t", | |
| 4 | − | "description": "How to use g1t: chat, agents, docs and code in one workspace.", | |
| 4 | + | "description": "How to use g1t, the code platform for your team and its coding agents.", | |
| 5 | 5 | "start_url": "/", | |
| 6 | 6 | "scope": "/", | |
| 7 | 7 | "display": "minimal-ui", |
| 45 | 45 | if (current && tabs && current.offsetLeft + current.offsetWidth > tabs.clientWidth) { | |
| 46 | 46 | tabs.scrollLeft = current.offsetLeft - (tabs.clientWidth - current.offsetWidth) / 2; | |
| 47 | 47 | } | |
| 48 | + | // The row fades out at an edge with more tabs past it, so a cut-off | |
| 49 | + | // tab reads as "scroll for more" rather than as a clipped word. | |
| 50 | + | const bar = document.querySelector<HTMLElement>('.g1t-tabs'); | |
| 51 | + | if (bar) { | |
| 52 | + | const edges = () => { | |
| 53 | + | const end = bar.scrollWidth - bar.clientWidth; | |
| 54 | + | bar.toggleAttribute('data-more-start', bar.scrollLeft > 1); | |
| 55 | + | bar.toggleAttribute('data-more-end', bar.scrollLeft < end - 1); | |
| 56 | + | }; | |
| 57 | + | edges(); | |
| 58 | + | bar.addEventListener('scroll', edges, { passive: true }); | |
| 59 | + | addEventListener('resize', edges); | |
| 60 | + | } | |
| 48 | 61 | </script> | |
| 49 | 62 | ||
| 50 | 63 | <style> | |
| ⋯ | |||
| 144 | 157 | .g1t-tabs::-webkit-scrollbar { | |
| 145 | 158 | display: none; | |
| 146 | 159 | } | |
| 160 | + | .g1t-tabs[data-more-start], | |
| 161 | + | .g1t-tabs[data-more-end] { | |
| 162 | + | --fade: 2.5rem; | |
| 163 | + | mask-image: linear-gradient( | |
| 164 | + | to right, | |
| 165 | + | transparent, | |
| 166 | + | #000 var(--fade-start, 0px), | |
| 167 | + | #000 calc(100% - var(--fade-end, 0px)), | |
| 168 | + | transparent | |
| 169 | + | ); | |
| 170 | + | } | |
| 171 | + | .g1t-tabs[data-more-start] { | |
| 172 | + | --fade-start: var(--fade); | |
| 173 | + | } | |
| 174 | + | .g1t-tabs[data-more-end] { | |
| 175 | + | --fade-end: var(--fade); | |
| 176 | + | } | |
| 147 | 177 | .g1t-tabs a { | |
| 148 | 178 | display: flex; | |
| 149 | 179 | flex-shrink: 0; | |
| 9 | 9 | ||
| 10 | 10 | const { entry, id } = Astro.locals.starlightRoute; | |
| 11 | 11 | const { title, description } = entry.data; | |
| 12 | − | const section = NAV.get(Astro.locals.starlightRoute)?.section; | |
| 12 | + | const nav = NAV.get(Astro.locals.starlightRoute)?.section; | |
| 13 | + | // No eyebrow that only repeats the title, as on Chat's page in Chat. | |
| 14 | + | const section = nav && nav.toLowerCase() !== title.toLowerCase() ? nav : undefined; | |
| 13 | 15 | const markdown = `/${id === '' || id === 'index' ? 'index' : id}.md`; | |
| 14 | 16 | const absolute = new URL(markdown, Astro.site).href; | |
| 15 | 17 | const ask = encodeURIComponent(`Read ${absolute} so I can ask questions about it.`); | |
| ⋯ | |||
| 250 | 252 | font-size: 2rem; | |
| 251 | 253 | overflow-wrap: anywhere; | |
| 252 | 254 | } | |
| 255 | + | /* The lead a step above the body, not a headline of its own. */ | |
| 256 | + | .g1t-description { | |
| 257 | + | margin-top: 0.75rem; | |
| 258 | + | font-size: 1.0625rem; | |
| 259 | + | line-height: 1.5; | |
| 260 | + | } | |
| 253 | 261 | .g1t-copy { | |
| 254 | 262 | height: 2.5rem; | |
| 255 | 263 | margin-top: 0; | |
| 165 | 165 | ||
| 166 | 166 | - Version update pull requests are opened for npm, Cargo, Go and pip only. | |
| 167 | 167 | Entries for other ecosystems are checked and listed, and open nothing. | |
| 168 | − | ||
| 169 | 168 | - A multi-ecosystem group opens one pull request per ecosystem, not one | |
| 170 | 169 | for the group. | |
| 171 | 170 | - Registries that sign in with OIDC are not used. | |
| 172 | 171 | ||
| 173 | − | - **Instead.** Keep a separate entry per ecosystem and directory, and | |
| 174 | − | check the Security page, which lists what each entry reads but does not | |
| 175 | − | act on. See [Dependency updates](/guides/dependency-updates/#options). | |
| 176 | − | - **Status.** Planned. | |
| 172 | + | **Instead.** Keep a separate entry per ecosystem and directory, and check | |
| 173 | + | the Security page, which lists what each entry reads but does not act on. | |
| 174 | + | See [Dependency updates](/guides/dependency-updates/#options). | |
| 175 | + | **Status.** Planned. | |
| 177 | 176 | ||
| 178 | 177 | ### Very large changes are shown in parts | |
| 179 | 178 | ||
| ⋯ | |||
| 270 | 269 | stop it in a later one; write a schedule's `cron` in UTC. | |
| 271 | 270 | - **Status.** Not scheduled. | |
| 272 | 271 | ||
| 273 | − | ### Self-hosted Windows and macOS jobs run on the host | |
| 272 | + | ### Self-hosted macOS jobs run on the host | |
| 274 | 273 | ||
| 275 | − | On a self-hosted runner, a job native to Windows or macOS runs directly on | |
| 276 | − | the machine, unsandboxed, in host mode, which is deprecated. Only Linux | |
| 277 | − | jobs run in a sandbox of their own, a container inside the runner's own | |
| 278 | − | container under Sysbox. | |
| 274 | + | On a self-hosted runner, a job native to macOS runs directly on the Mac, | |
| 275 | + | unsandboxed, in host mode, which is deprecated. Linux jobs run in a | |
| 276 | + | sandbox of their own on every computer, and Windows jobs in a Windows | |
| 277 | + | Sandbox per job on Windows 11 Pro, Enterprise and Education. Windows 11 | |
| 278 | + | Home has no Windows Sandbox, so it runs Linux jobs only. | |
| 279 | 279 | ||
| 280 | − | - **Why.** Sandboxes for them are not built yet: Windows containers with | |
| 281 | − | Hyper-V isolation, and ephemeral macOS virtual machines (two per Mac at | |
| 282 | − | most, by Apple's licence). | |
| 280 | + | - **Why.** Sandboxed macOS jobs are designed, not built: an ephemeral | |
| 281 | + | macOS virtual machine per job, at most two per Mac by Apple's licence. | |
| 283 | 282 | - **Instead.** Run Linux jobs in | |
| 284 | − | [the runner's container](/guides/self-hosted-runners/#run-it-as-a-container), | |
| 285 | − | and keep Windows and macOS runners for repositories whose code you | |
| 286 | − | trust. Host mode refuses public repositories' work. | |
| 283 | + | [the sandbox on your computer](/guides/self-hosted-runners/#sandbox-on-your-computer), | |
| 284 | + | and keep host-mode Mac runners for repositories whose code you trust. | |
| 285 | + | Host mode refuses public repositories' work. | |
| 287 | 286 | - **Status.** Planned. See | |
| 288 | − | [Windows and macOS](/guides/self-hosted-runners/#windows-and-macos). | |
| 287 | + | [how each computer runs it](/guides/self-hosted-runners/#how-each-computer-runs-it). | |
| 288 | + | ||
| 289 | + | ### The runner's sandbox is checked end to end on Windows only | |
| 289 | 290 | ||
| 291 | + | The sandbox for Linux jobs on macOS (a VM under Apple's Virtualization | |
| 292 | + | framework) and on Linux without Sysbox (a VM under QEMU with KVM) is | |
| 293 | + | built to the same design as on Windows, but has not yet been run on a | |
| 294 | + | Mac or on such a Linux machine. | |
| 295 | + | ||
| 296 | + | - **Instead.** Run `g1t-runner sandbox status` first: it says what the | |
| 297 | + | computer lacks. On Linux, the runner's container under Sysbox needs no | |
| 298 | + | VM. See [what it cannot stop](/guides/self-hosted-runners/#what-it-cannot-stop). | |
| 299 | + | - **Status.** Planned. | |
| 300 | + | ||
| 290 | 301 | ### A self-hosted job's Docker can reach its runner's container | |
| 291 | 302 | ||
| 292 | 303 | On a runner in its container under Sysbox, a job that uses Docker gets an | |
| ⋯ | |||
| 444 | 455 | ||
| 445 | 456 | ### Some costs are not fully defined yet | |
| 446 | 457 | ||
| 447 | − | Cloudflare's [Artifacts pricing documentation](https://developers.cloudflare.com/artifacts/platform/pricing/) | |
| 448 | − | lists October 14, 2026 as the billing start, while its launch announcement | |
| 449 | − | lists October 15. The pricing documentation identifies repository operations | |
| 450 | − | such as create, push, pull and clone, and storage measured from daily peaks. | |
| 451 | − | g1t's measurements still need reconciliation against the upstream bill. | |
| 458 | + | Cloudflare Artifacts, where every repository is stored, started billing in | |
| 459 | + | mid-October 2026. Its [pricing](https://developers.cloudflare.com/artifacts/platform/pricing/) | |
| 460 | + | counts repository operations such as create, push, pull and clone, and | |
| 461 | + | storage measured from daily peaks. g1t's own counts are still being | |
| 462 | + | reconciled against that bill. | |
| 452 | 463 | ||
| 453 | 464 | - **What g1t does.** It counts every clone, fetch and push through its git | |
| 454 | 465 | endpoints. Each day it checks what Cloudflare billed it for containers and | |
| ⋯ | |||
| 480 | 491 | cover personal access tokens only, not applications you signed in to with | |
| 481 | 492 | OAuth. **Status.** Planned. | |
| 482 | 493 | ||
| 483 | − | ### No uptime commitment during the beta | |
| 494 | + | ### No uptime commitment during the alpha | |
| 484 | 495 | ||
| 485 | 496 | g1t does not promise a particular uptime or offer a service level agreement | |
| 486 | 497 | unless you have a written agreement with us. Several of the Cloudflare | |
| ⋯ | |||
| 488 | 499 | [status.g1t.sh](https://status.g1t.sh/) shows how each part of g1t is doing, | |
| 489 | 500 | and every incident. Sandboxes are not checked there yet. See | |
| 490 | 501 | [Status and incidents](/guides/status/). **Status.** Not scheduled during | |
| 491 | − | the beta. | |
| 502 | + | the alpha. | |
| 492 | 503 | ||
| 493 | 504 | ### Self-hosting is early | |
| 494 | 505 | ||
| ⋯ | |||
| 500 | 511 | runner. Use the hosted service to try that integration loop. Self-hosting | |
| 501 | 512 | is not ready for the open internet. **Status.** Planned, in phases. | |
| 502 | 513 | ||
| 503 | − | ### Not built yet | |
| 514 | + | ### No wikis | |
| 504 | 515 | ||
| 505 | − | ||
| 506 | − | - **Wikis.** Not scheduled. Keep docs in the repository. | |
| 516 | + | A repository has no wiki. **Instead.** Keep docs in the repository, or | |
| 517 | + | write them together in [Artifacts](/guides/artifacts/). **Status.** Not | |
| 518 | + | scheduled. | |
| 7 | 7 | import Diagram from '../../../components/Diagram.astro'; | |
| 8 | 8 | ||
| 9 | 9 | Everything that runs code you did not write runs in a sandbox: workflow | |
| 10 | − | jobs, merge queue builds, deployments, dependency updates, backups. Three | |
| 11 | − | pieces share the work. | |
| 10 | + | jobs, merge checks, deployments, dependency updates, backups. Three pieces | |
| 11 | + | share the work. | |
| 12 | 12 | ||
| 13 | 13 | | Piece | Is | Does | | |
| 14 | 14 | | --- | --- | --- | | |
| ⋯ | |||
| 26 | 26 | <Diagram name="actions-job" /> | |
| 27 | 27 | ||
| 28 | 28 | 1. **Trigger.** `actions` reads its event queue, `g1t-events-actions`, ten | |
| 29 | − | at a time (step 1), and matches each event to the workflows read from | |
| 30 | − | the default branch's `.g1t/workflows` (or `.github/workflows`). The | |
| 31 | − | files are GitHub Actions syntax, unchanged; [GitHub | |
| 29 | + | at a time (step 1), and matches each event to the workflows in | |
| 30 | + | `.g1t/workflows` at the commit the event is about (the default branch | |
| 31 | + | for schedules, issues and manual runs; `services/actions/src/sync.rs`). | |
| 32 | + | g1t never reads `.github/workflows`. The files are GitHub Actions | |
| 33 | + | syntax, unchanged; [GitHub | |
| 32 | 34 | Actions](/guides/actions/) lists what is supported. | |
| 33 | 35 | 2. **Plan.** It expands matrices, orders jobs by `needs`, and reports the | |
| 34 | 36 | run as pending on the commit (steps 2 and 3). | |
| ⋯ | |||
| 148 | 150 | | Mode | Started by | Does | | |
| 149 | 151 | | --- | --- | --- | | |
| 150 | 152 | | `actions` | `actions` | A workflow job | | |
| 151 | − | | `queue` | `runner`, when the queue changes | Builds a merge queue candidate | | |
| 152 | 153 | | `mergecheck` | `runner`, when a pull request changes | Checks whether a pull request merges cleanly | | |
| 153 | 154 | | `deploy` | `deployments` | Builds an app ([Deployments](/architecture/deployments/)) | | |
| 154 | 155 | | `bump` | `security` | Makes a dependency update | | |
| 155 | 156 | | `backup` | `runner`'s sweep, every 5 minutes | Cuts a repository backup ([Git](/architecture/git/#backups)) | | |
| 156 | 157 | ||
| 158 | + | The merge queue needs no sandbox of its own: `work` builds each queued | |
| 159 | + | state in the repository itself ([Work](/architecture/work/#the-merge-queue)), | |
| 160 | + | and only the workflows that run on `merge_group` run in sandboxes, as | |
| 161 | + | workflow jobs. | |
| 162 | + | ||
| 157 | 163 | Compute is admitted through billing: a sandbox starts only after | |
| 158 | 164 | `billing` holds its estimated cost (`reserve`), and the hold is settled | |
| 159 | 165 | with the real cost when it ends (`settle`). | |
| 39 | 39 | | Field | Holds | | |
| 40 | 40 | | --- | --- | | |
| 41 | 41 | | `kind` | `User`, `Workspace`, `Agent` or `System`: what kind of principal this is | | |
| 42 | − | | `workspaces` | Each membership: the role, the workspace's base permission, organisation roles such as billing or security manager, and privileges | | |
| 42 | + | | `workspaces` | Each membership: the role, the workspace's base permission, the roles that add to a member, such as billing or security manager, and privileges | | |
| 43 | 43 | | `grants` | Roles on repositories, directly or through teams | | |
| 44 | 44 | | `acting` | For a job or run token: the narrower scope it acts within | | |
| 45 | 45 | ||
| ⋯ | |||
| 63 | 63 | ||
| 64 | 64 | | Level | Roles | | |
| 65 | 65 | | --- | --- | | |
| 66 | − | | Workspace | Owner and member, plus organisation roles (billing manager, security manager) and member privileges | | |
| 66 | + | | Workspace | Owner and member, plus the roles that add to a member (billing manager, security manager) and member privileges | | |
| 67 | 67 | | Repository | Read, Triage, Write, Maintain and Admin, given to a person, a team, or everyone in the workspace as its base permission | | |
| 68 | 68 | | Outside collaborators | A repository role for someone who is not a member | | |
| 69 | 69 | ||
| 38 | 38 | | `services/notify` | TypeScript | core | Notifications, live pages, presence, browser and phone push | A Durable Object per person, per workspace and per repository | `identity` | | |
| 39 | 39 | | `services/telemetry` | TypeScript | core | Logs, traces, metrics and errors for projects, at `otel.g1t.sh` | A Durable Object per project | `billing` | | |
| 40 | 40 | | `services/billing` | Rust | core | Usage, holds and settlements, limits, Stripe | D1 `g1t-billing` | `identity`, `packages`, `repos` | | |
| 41 | − | | `services/integrations` | Rust | core | Model providers, alert sources, issue trackers, the GitHub App, Ask g1t | D1 `g1t-integrations` | `identity`, `projects`, `repos`, `work` | | |
| 41 | + | | `services/integrations` | Rust | core | The workspace's AI providers and Ask g1t, alert sources, issue trackers, the GitHub App | D1 `g1t-integrations` | `identity`, `projects`, `repos`, `work` | | |
| 42 | 42 | | `services/webhooks` | Rust | core | Webhook subscriptions and signed deliveries with retries | D1 `g1t-webhooks` | `identity`, `repos` | | |
| 43 | 43 | | `services/og` | TypeScript | core | Social cards and page screenshots, at `og.g1t.sh` | The screenshots bucket | `billing`, `identity`, `projects`, `repos`, `work` | | |
| 44 | 44 |
| 95 | 95 | and whether the person asking may bypass it. A merge happens only when | |
| 96 | 96 | none fails or waits, for people and coding agents alike. | |
| 97 | 97 | ||
| 98 | − | When everything passes, `work` asks `repos` to land the pull request's | |
| 99 | − | head on the base, and publishes `pull.merged`. | |
| 98 | + | When everything passes, `work` asks `repos` to land the pull request on | |
| 99 | + | the base by the merge method asked for: a merge commit, squash, rebase or | |
| 100 | + | fast-forward (`services/repos/src/merge_methods.rs`). Then it publishes | |
| 101 | + | `pull.merged`. | |
| 100 | 102 | ||
| 101 | 103 | ## The merge queue | |
| 102 | 104 | ||
| ⋯ | |||
| 108 | 110 | <Diagram name="merge-queue" /> | |
| 109 | 111 | ||
| 110 | 112 | 1. **Build.** Up to `max_entries_to_build` entries (4 by default) are | |
| 111 | − | tested at once. For each, a sandbox merges the default branch with that | |
| 112 | − | entry and every entry ahead of it, and pushes the result to | |
| 113 | − | `g1t-queue/<entry>` in the repository. | |
| 113 | + | tested at once. Each entry is landed by its own merge method on the | |
| 114 | + | state of the entry ahead of it (the default branch, for the first), and | |
| 115 | + | the result is pushed to `g1t-queue/<entry>` in the repository. | |
| 114 | 116 | 2. **Test.** Workflows that run on `merge_group` run on each of those | |
| 115 | 117 | commits, and the required checks have to pass there. | |
| 116 | 118 | 3. **Land.** Entries land in order. Once everything ahead of it has landed, | |
| 1 | 1 | --- | |
| 2 | 2 | title: How g1t works | |
| 3 | − | description: How Code works on g1t, and how issues, pull requests, checks, review, merging and sessions fit together. | |
| 3 | + | description: How Code works on g1t, and how issues, pull requests, checks, review, merging and sessions fit together, for people and coding agents alike. | |
| 4 | 4 | --- | |
| 5 | 5 | ||
| 6 | − | g1t is where a team's code, conversations and shipping live. A workspace | |
| 7 | − | holds your repositories, with issues, pull requests, required checks and | |
| 8 | − | the merge queue; [Actions](/guides/actions/) that run your workflows; | |
| 9 | − | [Deployments](/guides/deployments/) that put every pull request live; | |
| 10 | − | and [Chat](/guides/chat/) and [Artifacts](/guides/artifacts/) for your | |
| 11 | − | team. This page is about **Code**, where changes are made and landed. | |
| 6 | + | A workspace holds your repositories, with issues, pull requests, required | |
| 7 | + | checks and the merge queue; [Actions](/guides/actions/) that run your | |
| 8 | + | workflows; [Deployments](/guides/deployments/) that put every pull request | |
| 9 | + | live; and [Chat](/guides/chat/) for your team. This page is about | |
| 10 | + | **Code**, where changes are made and landed. | |
| 11 | + | ||
| 12 | + | Underneath is ordinary git: clone, branch, commit, push and pull work as | |
| 13 | + | they do anywhere. On top of it are the two things you already know, | |
| 14 | + | **issues** and **pull requests**, built so that many people and coding | |
| 15 | + | agents can work at once without getting in each other's way. | |
| 12 | 16 | ||
| 13 | 17 | The coding agents you already use, such as Claude Code, Codex, Cursor or | |
| 14 | 18 | OpenCode, work on g1t from wherever they run. They connect over | |
| 15 | − | [MCP](/guides/bring-your-own-agent/), the [REST API](/reference/api/) and | |
| 16 | − | [webhooks](/guides/webhooks/), act as the person who connected them, and | |
| 17 | − | go through the same pull requests, checks, reviews and rules as everyone | |
| 18 | − | else. | |
| 19 | − | ||
| 20 | − | Underneath it is ordinary git: repositories, commits, branches, clone, push | |
| 21 | − | and pull all work as they do anywhere. On top of that it has the two things | |
| 22 | − | you already know, **issues** and **pull requests**, built | |
| 23 | − | so that many people and agents can work at once without getting in each | |
| 24 | − | other's way. | |
| 19 | + | [MCP](/guides/bring-your-own-agent/), the [g1t CLI](/guides/cli/), the | |
| 20 | + | [REST API](/reference/api/) and [webhooks](/guides/webhooks/), act as the | |
| 21 | + | person who connected them, and go through the same pull requests, checks, | |
| 22 | + | reviews and rules as everyone else. | |
| 25 | 23 | ||
| 26 | 24 | | | What it is | | |
| 27 | 25 | | --- | --- | | |
| 28 | 26 | | **Issue** | What should change: a bug, a feature, a question. | | |
| 29 | − | | **Pull request** | A proposed change, in its own fork or on a branch. Usually made for an issue. | | |
| 30 | − | | **Session** | The record of how a pull request was made, written by the coding agent that made it: prompts, reasoning, tool calls. | | |
| 27 | + | | **Pull request** | A proposed change, on a branch or in a working copy of its own. Usually made for an issue. | | |
| 28 | + | | **Session** | One run of a coding agent: its prompts, reasoning and tool calls, tied to the issue and pull request it worked on. | | |
| 31 | 29 | ||
| 32 | − | What g1t adds: one issue can have several pull requests, each from a | |
| 33 | − | different person or agent, and g1t keeps track of which one was merged. | |
| 30 | + | One issue can have several pull requests, each from a different person or | |
| 31 | + | agent, and g1t keeps track of which one was merged. | |
| 34 | 32 | ||
| 35 | 33 | ## Issues | |
| 36 | 34 | ||
| 37 | − | An issue says what should change in a repository. People open them, coding | |
| 38 | − | agents open them, and so can anything with an access token, such as an error | |
| 39 | − | tracker reporting a crash. | |
| 35 | + | An issue says what should change in a repository. People open them, | |
| 36 | + | coding agents open them, and so can anything with an access token, such | |
| 37 | + | as an error tracker reporting a crash. | |
| 40 | 38 | ||
| 41 | 39 | An issue has: | |
| 42 | 40 | ||
| 43 | 41 | - a **title** and a **description** in Markdown. Whoever works on the | |
| 44 | − | issue, person or agent, works from this text. It can say what done means | |
| 45 | − | in plain words, under a `## Definition of done` heading if you like: | |
| 42 | + | issue, person or agent, works from this text. Say what done means in | |
| 43 | + | plain words, under a `## Definition of done` heading if you like: it is | |
| 46 | 44 | context for whoever works on it and its reviewers, not something a merge | |
| 47 | 45 | waits on; | |
| 48 | 46 | - **labels**, which say what kind of issue it is; | |
| 49 | − | - **comments**; | |
| 47 | + | - **comments** and **assignees**; | |
| 50 | 48 | - a **state**: open or closed. A closed issue records why: `completed` or | |
| 51 | 49 | `not_planned`. | |
| 52 | 50 | ||
| 53 | 51 | Issues and pull requests share one sequence of numbers per repository, so | |
| 54 | 52 | `#12` names exactly one of them. | |
| 55 | 53 | ||
| 56 | − | ### Labels | |
| 57 | − | ||
| 58 | − | Every repository starts with `bug`, `feature`, `docs`, `chore` and | |
| 59 | − | `question`. There is nothing to set up for others: putting a new name on an | |
| 60 | − | issue creates the label. Labels are lowercase, and an issue can carry up to | |
| 61 | − | ten. | |
| 62 | − | ||
| 63 | − | Filter a repository's issues by label on the site, or with `?label=` in the | |
| 64 | − | API. | |
| 54 | + | A new repository starts with a set of labels such as `bug`, | |
| 55 | + | `enhancement` and `question`; anyone with the Triage role can make more as | |
| 56 | + | they label. An issue or pull request carries up to 20. See | |
| 57 | + | [labels](/guides/labels/). | |
| 65 | 58 | ||
| 66 | 59 | ## Pull requests | |
| 67 | 60 | ||
| 68 | 61 | A pull request is a proposed change. There are two ways to make one. | |
| 69 | 62 | ||
| 70 | − | **In a fork.** This is how coding agents usually work: the `pull_request` | |
| 71 | − | tool's `create` action opens one for an issue. Opening the pull request creates a | |
| 72 | − | copy of the repository that belongs to that pull request alone, ready in | |
| 73 | − | about the time a branch would be. Its author clones the fork, commits and pushes to it. Nothing they do | |
| 74 | − | can touch `main` or another pull request. The fork lives at | |
| 75 | − | `g1t.sh/pulls/<pull request id>.git` and is exactly as visible as the | |
| 76 | − | repository it came from. The pull request starts as a draft. | |
| 63 | + | **From a branch.** Push a branch to the repository, then open a pull | |
| 64 | + | request from it on the **Pull requests** tab or with `g1t pr create`. It | |
| 65 | + | needs write access to the repository, and is ready for review as soon as | |
| 66 | + | it is opened. | |
| 77 | 67 | ||
| 78 | − | **From a branch.** This is the way you already know. Push a branch to the | |
| 79 | − | repository, then open a pull request from it on the **Pull requests** tab. | |
| 80 | − | It needs write access to the repository, and it is ready for review as soon | |
| 81 | − | as it is opened. | |
| 68 | + | **In a working copy.** This is how coding agents usually work: the | |
| 69 | + | `pull_request` tool's `create` action, or `g1t pr create --issue 12` with | |
| 70 | + | no branch of your own, opens a draft pull request for an issue and makes | |
| 71 | + | it a copy of the repository that belongs to that pull request alone, | |
| 72 | + | ready in about the time a branch would be. The author clones it, commits | |
| 73 | + | and pushes to it, and nothing they do can touch `main` or another pull | |
| 74 | + | request. It lives at `g1t.sh/pulls/<pull request id>.git` and is exactly | |
| 75 | + | as visible as the repository it came from. | |
| 82 | 76 | ||
| 83 | − | [Forks and working copies](/concepts/forks/) explains when each is the better | |
| 84 | − | choice. | |
| 77 | + | [Forks and working copies](/concepts/forks/) explains when each is the | |
| 78 | + | better choice. | |
| 85 | 79 | ||
| 86 | 80 | | Status | Meaning | | |
| 87 | 81 | | --- | --- | | |
| 88 | − | | `draft` | Still being worked on. A pull request with a fork starts here. | | |
| 82 | + | | `draft` | Still being worked on. A pull request in a working copy starts here. | | |
| 89 | 83 | | `open` | Ready for review, with a description of what changed and why. | | |
| 90 | − | | `merged` | Landed on `main`. | | |
| 84 | + | | `merged` | Landed on its base branch. | | |
| 91 | 85 | | `closed` | Closed without merging. | | |
| 92 | 86 | ||
| 93 | − | A pull request is normally opened **for an issue**. It can also stand alone, | |
| 94 | − | with its own title, for a change nobody filed an issue about. | |
| 87 | + | A pull request is normally opened **for an issue**. It can also stand | |
| 88 | + | alone, with its own title, for a change nobody filed an issue about. | |
| 95 | 89 | ||
| 96 | 90 | ## Assignees and reviewers | |
| 97 | 91 | ||
| 98 | − | An issue is assigned to people. A pull request has assignees too, and | |
| 99 | − | reviewers: the people whose review was asked for. Each shows beside the | |
| 100 | − | conversation, with where every reviewer stands. | |
| 92 | + | Issues and pull requests have assignees, and pull requests have | |
| 93 | + | reviewers: the people and teams whose review was asked for. Each shows | |
| 94 | + | beside the conversation, with where every reviewer stands. | |
| 95 | + | ||
| 96 | + | Assigning an issue to `g1t`, or mentioning `@g1t` in a comment, starts a | |
| 97 | + | session on the workspace's own Anthropic account, as the person who asked; | |
| 98 | + | see [Assign g1t](/guides/sessions/#assign-g1t-or-ask-it-in-the-thread). | |
| 101 | 99 | ||
| 102 | − | Whatever happens is told in the conversation, in order, between the | |
| 103 | − | comments: who assigned whom, whose review was asked for, and when it was | |
| 104 | − | marked ready, merged or closed. | |
| 100 | + | The conversation tells what happened, in order, between the comments: who | |
| 101 | + | assigned whom, whose review was asked for, which sessions started, and | |
| 102 | + | when it was marked ready, merged or closed. | |
| 105 | 103 | ||
| 106 | 104 | ## Several pull requests for one issue | |
| 107 | 105 | ||
| 108 | 106 | An issue can have more than one pull request: a second attempt after the | |
| 109 | − | first fell short, or two agents trying different approaches. Each is in its own | |
| 110 | − | fork, with its own session and its own diff. The issue's page lists them | |
| 111 | − | all with their status. | |
| 107 | + | first fell short, or two agents trying different approaches. Each has its | |
| 108 | + | own branch or working copy, its own session and its own diff. The issue's | |
| 109 | + | page lists them all with their status. | |
| 112 | 110 | ||
| 113 | 111 | When you merge one: | |
| 114 | 112 | ||
| ⋯ | |||
| 121 | 119 | So the answer to "which one did we take?" is on the issue, on the merged | |
| 122 | 120 | pull request, and on each one that was passed over. | |
| 123 | 121 | ||
| 124 | − | Sometimes several pull requests each do part of an issue. When merging, say | |
| 125 | − | that the issue should stay open. The pull request merges, and the issue and | |
| 122 | + | When several pull requests each do part of an issue, say that the issue | |
| 123 | + | should stay open as you merge. The pull request merges, and the issue and | |
| 126 | 124 | the other pull requests are left as they are. | |
| 127 | 125 | ||
| 128 | 126 | ## Checks | |
| 129 | 127 | ||
| 130 | 128 | A pull request's checks are what the repository's | |
| 131 | − | [workflows](/guides/actions/) report on its head commit. Every workflow | |
| 132 | − | that runs on `pull_request` runs on every pull request, whoever opened it, | |
| 133 | − | a person or an agent, and reports a check named after the workflow, such as | |
| 129 | + | [workflows](/guides/actions/) and integrations report on its head commit. | |
| 130 | + | Every workflow that runs on `pull_request` runs on every pull request, | |
| 131 | + | whoever opened it, and reports a check named after the workflow, such as | |
| 134 | 132 | `CI`. | |
| 135 | 133 | ||
| 136 | − | The default branch decides which checks a merge needs: its | |
| 134 | + | The base branch decides which checks a merge needs: its | |
| 137 | 135 | [required status checks](/guides/pull-requests/#required-status-checks). | |
| 138 | 136 | A pull request merges only once each of them has passed on its head. One | |
| 139 | 137 | that failed, is still running or has not reported yet holds the merge, | |
| 140 | 138 | unless the repository allows bypassing them and someone who can merge | |
| 141 | 139 | chooses to. Checks that are not required are shown, and never hold a merge. | |
| 142 | 140 | ||
| 143 | − | The same rules hold for people and agents. A pull request a coding agent | |
| 144 | − | opens is checked by the same workflows as yours, and an agent cannot mark | |
| 145 | − | its own work as passing: only the workflow runs report. | |
| 141 | + | The same rules hold for people and agents. A pull request an agent opens | |
| 142 | + | is checked by the same workflows as yours, and an agent cannot mark its | |
| 143 | + | own work as passing: only the checks report. | |
| 146 | 144 | ||
| 147 | 145 | ## Review | |
| 148 | 146 | ||
| 149 | 147 | Anyone who can see a pull request can comment on it, on the whole of it or | |
| 150 | − | on a single line of its change. Line comments are shown in the **Files changed** | |
| 151 | − | tab under the line they are about. | |
| 148 | + | on a single line of its change, in **Files changed**. | |
| 152 | 149 | ||
| 153 | 150 | A reviewer can also give a verdict: **approve**, or **request changes**. | |
| 154 | − | The pull request shows where each reviewer stands. You cannot give a verdict | |
| 155 | − | on a pull request you opened, and that holds for a coding agent working as | |
| 156 | − | you too. | |
| 151 | + | You cannot give a verdict on a pull request you opened, and neither can a | |
| 152 | + | coding agent working as you. | |
| 157 | 153 | ||
| 158 | 154 | ## Overlap | |
| 159 | 155 | ||
| 160 | − | When many changes are in flight, some touch the same files. g1t keeps track | |
| 161 | − | of which files each pull request changes, from every push, and shows on a | |
| 156 | + | When many changes are in flight, some touch the same files. g1t tracks | |
| 157 | + | which files each pull request changes, from every push, and shows on a | |
| 162 | 158 | pull request which others in progress change the same ones. | |
| 163 | 159 | ||
| 164 | 160 | Two pull requests for the *same* issue are expected to overlap: they are | |
| 165 | − | alternatives, and one will be merged. Two for *different* issues are heading | |
| 166 | − | for a conflict, and g1t says so while the work is still going on rather than | |
| 167 | − | when the second one tries to merge. Coding agents get the same list from | |
| 168 | − | the `pull_request` tool's `get` action, as `overlaps`, so they can keep | |
| 169 | − | their edits small and local where they have to touch the same files. | |
| 161 | + | alternatives, and one will be merged. Two for *different* issues are | |
| 162 | + | heading for a conflict, and g1t says so while the work is still going on, | |
| 163 | + | not when the second one tries to merge. Agents get the same list from the | |
| 164 | + | `pull_request` tool's `get` action, as `overlaps`, so they can keep their | |
| 165 | + | edits small where they must touch the same files. | |
| 170 | 166 | ||
| 171 | 167 | ## Merging | |
| 172 | 168 | ||
| 173 | − | Someone with the [Write role](/guides/access-and-roles/) or higher on the | |
| 174 | − | repository merges a pull request once it is | |
| 175 | − | marked ready and its [required checks](/guides/pull-requests/#required-status-checks) | |
| 176 | − | have passed. Merging moves `main` to the pull | |
| 177 | − | request's head commit, or, in a repository that merges through | |
| 178 | − | [the merge queue](/guides/merge-queue/), adds it to the queue. | |
| 169 | + | Someone with the [Write role](/guides/access-and-roles/) or higher merges | |
| 170 | + | a pull request once it is ready and its | |
| 171 | + | [required checks](/guides/pull-requests/#required-status-checks) have | |
| 172 | + | passed. It lands by one of the repository's | |
| 173 | + | [merge methods](/guides/pull-requests/#merge-methods): a merge commit, a | |
| 174 | + | squash, a rebase or a fast-forward. In a repository that uses | |
| 175 | + | [the merge queue](/guides/merge-queue/), merging adds it to the queue | |
| 176 | + | instead. **Auto-merge** lands it by itself once everything it needs is met. | |
| 179 | 177 | ||
| 180 | − | `main` only moves forward to a commit that contains everything already on | |
| 181 | − | it. If something else landed first, the pull request is **behind**, and its | |
| 182 | − | page says so. Merging it then brings it up to date first and lands it once | |
| 183 | − | that is done, unless the repository requires pull requests to be up to date | |
| 184 | − | before they merge; then merging is refused until it has caught up. | |
| 178 | + | When something else landed first, the pull request is **behind**, and its | |
| 179 | + | page says so. Merging brings it up to date and lands it, unless the | |
| 180 | + | repository requires pull requests to be up to date first; then merging | |
| 181 | + | waits until it has caught up. | |
| 185 | 182 | ||
| 186 | − | Updating the branch brings it up to date. When the pull request's and `main`'s | |
| 187 | − | changes do not clash (edits to the same file merge line by line), g1t merges | |
| 188 | − | or rebases `main` in itself and pushes the result in a few seconds, and the | |
| 189 | − | workflows run again on it | |
| 190 | − | ([how updating works](/guides/pull-requests/#keeping-it-up-to-date)). When they | |
| 191 | − | changed the same lines, do it by hand: pull `main` into the fork or | |
| 192 | − | the branch, resolve, and push. `main` never loses a commit this way, however many | |
| 193 | − | pull requests are in flight. | |
| 183 | + | **Update branch** brings it up to date. When the two sides' changes do | |
| 184 | + | not clash (edits to the same file merge line by line), g1t merges or | |
| 185 | + | rebases the base in itself in a few seconds, and the workflows run again | |
| 186 | + | ([keeping it up to date](/guides/pull-requests/#keeping-it-up-to-date)). | |
| 187 | + | When they changed the same lines, pull the base into the branch or | |
| 188 | + | working copy, resolve, and push. The base never loses a commit this way, | |
| 189 | + | however many pull requests are in flight. | |
| 194 | 190 | ||
| 195 | 191 | ## The merge queue | |
| 196 | 192 | ||
| 197 | − | Merging one pull request at a time keeps every merge clean as text, but two | |
| 198 | − | changes can merge without a conflict and still break each other. A | |
| 193 | + | Merging one pull request at a time keeps every merge clean as text, but | |
| 194 | + | two changes can merge without a conflict and still break each other. A | |
| 199 | 195 | repository whose rules include **Require the merge queue** (**Settings → | |
| 200 | − | Rules**) tests each pull request | |
| 201 | − | together with the ones ahead of it, and `main` only moves to a state whose | |
| 202 | − | required checks passed. | |
| 203 | − | See [merge queue](/guides/merge-queue/). | |
| 196 | + | Rules**) tests each pull request together with the ones ahead of it, and | |
| 197 | + | the base only moves to a state whose required checks passed. When one | |
| 198 | + | fails in combination, g1t says which pull requests and which check, so its | |
| 199 | + | author, person or agent, can fix it. See [merge queue](/guides/merge-queue/). | |
| 204 | 200 | ||
| 205 | 201 | ## Sessions and why-blame | |
| 206 | 202 | ||
| 207 | − | A session is the record of how a pull request was made: the prompt the | |
| 208 | − | agent was given, its reasoning, the tools it called and what they returned. | |
| 209 | − | The coding agent records it with the `pull_request` tool's `record_session` | |
| 210 | − | action as it works. | |
| 211 | − | Each entry is tied to the commit that was the head when it was recorded, so | |
| 212 | − | **Blame** on any file can show not only the commit that last changed a | |
| 203 | + | A session is one run of a coding agent: the prompts it was given, its | |
| 204 | + | reasoning, the tools it called, and what it touched. g1t's | |
| 205 | + | [Claude Code plugin](/guides/claude-code-plugin/) records one for every | |
| 206 | + | Claude Code session in a g1t repository; other agents record theirs with | |
| 207 | + | the `session` tool or `g1t session`. A session is tied to the issue it | |
| 208 | + | claimed and the pull request it opened, and people read it on the pull | |
| 209 | + | request's **Session** tab and on the repository's **Work** page, which | |
| 210 | + | shows who is working on what right now. See | |
| 211 | + | [sessions, claims and dependencies](/guides/sessions/). | |
| 212 | + | ||
| 213 | + | Each entry is tied to the commit that was the head when it was recorded, | |
| 214 | + | so **Blame** on any file can show not only the commit that last changed a | |
| 213 | 215 | line, but the pull request and issue it came from and the agent's own | |
| 214 | 216 | account of the change. See [sessions and why-blame](/guides/why-blame/). | |
| 215 | 217 | ||
| 216 | 218 | ## Events | |
| 217 | 219 | ||
| 218 | 220 | Every state change in g1t is published as an event: a push, an issue being | |
| 219 | − | opened, a pull request being merged, a session growing. Events are delivered | |
| 220 | − | to the services that react to them and are kept as a timeline per | |
| 221 | − | repository, which you can read through the [API](/reference/api/). | |
| 221 | + | opened, a pull request being merged, a session growing. Events reach the | |
| 222 | + | services that react to them, your [webhooks](/guides/webhooks/), and a | |
| 223 | + | timeline per repository that you can read through the | |
| 224 | + | [API](/reference/api/) or follow with `g1t work events --follow`. | |
| 222 | 225 | [Events](/architecture/events/) in the Architecture section shows how. | |
| 223 | 226 | ||
| 224 | 227 | ## What is not built yet | |
| 225 | 228 | ||
| 226 | − | g1t is under active development. Every current limit, and why, is on | |
| 229 | + | Every current limit, and why, is on | |
| 227 | 230 | [What g1t can't do yet](/about/limitations/). | |
| 5 | 5 | ||
| 6 | 6 | import AgentSetup from '../../../components/AgentSetup.astro'; | |
| 7 | 7 | ||
| 8 | − | g1t is where your coding agents do their work. Claude Code, Codex, Cursor, | |
| 9 | − | OpenCode and any other harness connect to g1t the same way a person's tools | |
| 10 | − | do, and act with the access you give them: | |
| 8 | + | Claude Code, Codex, Cursor, OpenCode and any other harness connect to g1t | |
| 9 | + | the way a person's tools do, and act as you, with the access you give | |
| 10 | + | them: | |
| 11 | 11 | ||
| 12 | 12 | | To | Use | | |
| 13 | 13 | | --- | --- | | |
| 14 | 14 | | Read and change anything on g1t from inside the agent | The MCP server at `https://mcp.g1t.sh`: [MCP tools](/reference/mcp/) | | |
| 15 | − | | Push code | git over HTTPS, with an [access token](/guides/authentication/#access-tokens) | | |
| 15 | + | | Work from the agent's shell | The [g1t CLI](/guides/cli/): `g1t pr create`, `g1t pr checks --watch`, `g1t run logs --failed` | | |
| 16 | + | | Push code | git over HTTPS, signed in with `g1t auth setup-git` or an [access token](/guides/authentication/#access-tokens) | | |
| 16 | 17 | | Start the agent when something happens | [Webhooks](/guides/webhooks/), or a [workflow](/guides/actions/) on `issues` or `issue_comment` | | |
| 17 | 18 | | Show people how a change was made | The pull request's [session](/guides/why-blame/#sessions) | | |
| 18 | 19 | | See who is working on what, and not collide | [Sessions, claims and dependencies](/guides/sessions/) | | |
| 19 | 20 | | Script anything else | The [REST API](/reference/api/), which has the same operations as the tools | | |
| 20 | 21 | ||
| 21 | 22 | The agent runs where you run it: on your machine, in your CI, or on a | |
| 22 | − | [runner](/guides/self-hosted-runners/) of your own. g1t keeps the code, | |
| 23 | − | the issues and pull requests, the checks and the record of what happened. | |
| 23 | + | [runner](/guides/self-hosted-runners/) of your own, on your own model | |
| 24 | + | account. g1t runs no agents of its own; it keeps the code, the issues and | |
| 25 | + | pull requests, the checks and the record of what happened. To start one | |
| 26 | + | from g1t instead, see [hand an issue to g1t](#hand-an-issue-to-g1t). | |
| 24 | 27 | ||
| 25 | 28 | ## Connect your coding agent | |
| 26 | 29 | ||
| ⋯ | |||
| 40 | 43 | [Settings → Connected applications](https://g1t.sh/settings/applications), | |
| 41 | 44 | where you can change what it may do, or sign it out. | |
| 42 | 45 | ||
| 43 | − | The agent also needs to push with git, which asks for a username and a | |
| 44 | − | password: use your g1t username and an | |
| 45 | − | [access token](/guides/authentication/#access-tokens) with `code:write`. | |
| 46 | + | The agent also needs to push with git. Where the [g1t CLI](/guides/cli/) | |
| 47 | + | is signed in, `g1t auth setup-git` makes git use the same sign-in. | |
| 48 | + | Otherwise git asks for a username and a password: use your g1t username | |
| 49 | + | and an [access token](/guides/authentication/#access-tokens) with | |
| 50 | + | `code:write`. | |
| 46 | 51 | ||
| 47 | 52 | Ask it to list the open issues on a repository, or to work on one, and it | |
| 48 | 53 | will use g1t's tools. It sees only the tools and actions its access allows. | |
| ⋯ | |||
| 61 | 66 | ||
| 62 | 67 | | Choose | For an agent | | |
| 63 | 68 | | --- | --- | | |
| 64 | − | | Scopes | The **Agent** preset: reading across g1t, and writing code, issues, pull requests and notifications. Add an admin scope only for a task that needs it. | | |
| 69 | + | | Scopes | The **Agent** [preset](/guides/authentication/#presets), which reads across g1t and writes what an agent's work needs, and never includes an admin scope. Add one only for a task that needs it. | | |
| 65 | 70 | | Expires | The shortest that fits the work, such as 30 days. | | |
| 66 | 71 | ||
| 67 | 72 | A token reaches every workspace and repository you can. To keep an agent | |
| ⋯ | |||
| 82 | 87 | and what it is blocked by. Then `issue` with `claim`, which also says | |
| 83 | 88 | who else is on it. | |
| 84 | 89 | 2. `pull_request` with `create` and the issue's number. This opens a draft | |
| 85 | − | pull request and returns the git remote of its fork. | |
| 86 | − | 3. Clone the fork, make changes, commit and push. | |
| 90 | + | pull request and returns the git remote of its [working copy](/concepts/forks/#working-copies-for-pull-requests). | |
| 91 | + | 3. Clone the working copy, make changes, commit and push. | |
| 87 | 92 | 4. `pull_request` with `record_session` as it goes, so people can see its | |
| 88 | 93 | reasoning. | |
| 89 | 94 | 5. `pull_request` with `get`, to check `overlaps`, `behind` and the checks. | |
| ⋯ | |||
| 124 | 129 | says why; push a fix and the workflows run again. Run the same tests and | |
| 125 | 130 | linters the workflows run before you push, and you will rarely need to. | |
| 126 | 131 | ||
| 127 | − | If merging reports that `main` has moved, pull `main` from the repository | |
| 128 | − | into the fork and push. The pull request can then be merged, or joins the | |
| 129 | − | [merge queue](/guides/merge-queue/) when the repository uses one. | |
| 132 | + | When `main` has moved, `pull_request` with `update_branch` brings the pull | |
| 133 | + | request up to date in a few seconds. When the changes clash, pull `main` | |
| 134 | + | into the working copy, resolve and push. The pull request can then be | |
| 135 | + | merged, or joins the [merge queue](/guides/merge-queue/) when the | |
| 136 | + | repository uses one. | |
| 130 | 137 | ||
| 131 | 138 | When the merge queue takes an agent's pull request out because it fails | |
| 132 | 139 | combined with the pull requests ahead of it, g1t | |
| ⋯ | |||
| 186 | 193 | ``` | |
| 187 | 194 | ||
| 188 | 195 | Its hooks record your prompts, the tools Claude uses and what it called | |
| 189 | − | them with, and its pushes, through the [g1t CLI](/guides/cli/), and title | |
| 190 | − | each session with what its first prompt asked for. They never send what a | |
| 196 | + | them with, and its pushes, through the [g1t CLI](/guides/cli/), which | |
| 197 | + | must be installed and signed in (`g1t auth login`), and title each | |
| 198 | + | session with what its first prompt asked for. They never send what a | |
| 191 | 199 | tool returned. The plugin also gives Claude the issue's work context when | |
| 192 | 200 | a session starts (or, with no issue, when to claim or open one), g1t's MCP | |
| 193 | 201 | server, and commands to claim and hand off issues. See [Claude Code plugin](/guides/claude-code-plugin/) for | |
| ⋯ | |||
| 207 | 215 | issue will become a conflict for whichever merges second, so narrow the | |
| 208 | 216 | change, or say so in the pull request. | |
| 209 | 217 | - `behind`: whether `main` has moved since the pull request was made. If | |
| 210 | − | it has, pull `main` into the fork and push before asking for a merge. | |
| 218 | + | it has, `update_branch` before asking for a merge. | |
| 211 | 219 | ||
| 212 | 220 | Required checks, [rules](/guides/rules/) and the | |
| 213 | 221 | [merge queue](/guides/merge-queue/) hold for an agent's pull request | |
| 214 | 222 | exactly as for a person's. | |
| 215 | 223 | ||
| 224 | + | ## Hand an issue to g1t | |
| 225 | + | ||
| 226 | + | g1t runs no model of its own, but it can start a session for you on the | |
| 227 | + | workspace's own Anthropic account. Once an owner adds an Anthropic key | |
| 228 | + | under [**AI**](/guides/ai/): | |
| 229 | + | ||
| 230 | + | - **Assign to g1t** in an issue's sidebar (or `g1t issue edit 12 | |
| 231 | + | --add-assignee g1t`), or mention `@g1t` in a comment on an issue or pull | |
| 232 | + | request, with what you want done. | |
| 233 | + | - **Start a session** on an issue's page, `g1t work dispatch 12`, or the | |
| 234 | + | `session` tool's `dispatch` action, to choose the mode (`plan`, | |
| 235 | + | `implement` or `review`). | |
| 236 | + | ||
| 237 | + | g1t reacts 👀, then starts the session as you, with a token for that | |
| 238 | + | repository only that is revoked when the session ends. It installs the | |
| 239 | + | g1t CLI, clones the repository and works the way this page describes; its | |
| 240 | + | card shows in the issue's conversation, and Anthropic bills the account | |
| 241 | + | for it. Without an Anthropic key, `g1t work dispatch` prints a Claude Code | |
| 242 | + | command to run yourself instead. See | |
| 243 | + | [Assign g1t](/guides/sessions/#assign-g1t-or-ask-it-in-the-thread) and | |
| 244 | + | [Start a session on your Anthropic account](/guides/sessions/#start-a-session-on-your-anthropic-account). | |
| 245 | + | ||
| 216 | 246 | ## Start an agent when something happens | |
| 217 | 247 | ||
| 218 | 248 | To run your agent without anyone starting it by hand, react to g1t's | |
| ⋯ | |||
| 309 | 339 | | `access` | Who has which role on a repository, and deploy keys | | |
| 310 | 340 | | `team` | Teams and their members and repositories | | |
| 311 | 341 | | `workspace` | Workspaces, members, integrations, projects, rulesets and token rules | | |
| 342 | + | | `ai` | The workspace's AI providers, and what Ask answers with | | |
| 312 | 343 | | `billing` | Usage, budget and invoices | | |
| 313 | 344 | | `notifications` | Your inbox | | |
| 314 | 345 | | `account` | Who the token acts as, and your invitations | | |
| 60 | 60 | ||
| 61 | 61 | The plugin works in any working copy whose git remote is on g1t.sh, or on | |
| 62 | 62 | the host in `G1T_HOST` for a self-hosted g1t. Everywhere else its hooks do | |
| 63 | − | nothing. | |
| 63 | + | nothing. A session that starts in a folder with no g1t remote is recorded | |
| 64 | + | once it has one: from the next prompt, or the `git remote`, `git push` or | |
| 65 | + | `g1t repo create` command that added it, titled by its first prompt. | |
| 64 | 66 | ||
| 65 | 67 | ### Update | |
| 66 | 68 | ||
| ⋯ | |||
| 198 | 200 | the parent's session as `--parent`, its kind (`Explore`, your own | |
| 199 | 201 | subagents' names) and the task Claude gave it: the description and | |
| 200 | 202 | prompt of the Agent call, kept from the moment Claude made it | |
| 201 | − | (`PreToolUse`). The subagent is told it has a session of its own, to | |
| 203 | + | (`PreToolUse`) and matched to the subagent by the call's id or its | |
| 204 | + | prompt, so subagents started together each get their own. The subagent is told it has a session of its own, to | |
| 202 | 205 | look at its siblings with `g1t session list --issue N --subagents` | |
| 203 | 206 | before changing anything, and to claim only what it takes. | |
| 204 | 207 | - **While it works**, what it does is recorded in its own session, not its | |
| ⋯ | |||
| 269 | 272 | out of date, and said the same way. | |
| 270 | 273 | ||
| 271 | 274 | A CLI older than 0.3.3 records sessions but cannot acknowledge | |
| 272 | − | [coordination updates](#working-on-g1t). The hooks then show | |
| 275 | + | [coordination updates](/guides/sessions/#pending-coordination-updates). The hooks then show | |
| 273 | 276 | pending updates only when they change, not before every prompt, and ask | |
| 274 | 277 | Claude to tell you once to update the CLI. | |
| 275 | 278 | ||
| 178 | 178 | Commands about a repository work on the one the current directory is a | |
| 179 | 179 | clone of, found from its git remotes: `upstream` if it is on g1t, then | |
| 180 | 180 | `origin`, then the first one that is. In a pull request's own [working | |
| 181 | − | copy](/guides/pull-requests/), they work on the repository the pull request | |
| 181 | + | copy](/concepts/forks/#working-copies-for-pull-requests), they work on the repository the pull request | |
| 182 | 182 | is for. | |
| 183 | 183 | ||
| 184 | 184 | Anywhere else, or to choose another, pass `--repo` (`-R`), or set | |
| ⋯ | |||
| 191 | 191 | ||
| 192 | 192 | ## Everyday work | |
| 193 | 193 | ||
| 194 | + | ### Repositories | |
| 195 | + | ||
| 196 | + | ```sh | |
| 197 | + | g1t repo create my-project --source . --remote g1t # make it, add it as a remote, push | |
| 198 | + | g1t repo create acme/web --public --clone | |
| 199 | + | g1t repo list acme | |
| 200 | + | g1t repo view acme/web --web | |
| 201 | + | g1t repo clone acme/web | |
| 202 | + | g1t repo fork acme/web --clone | |
| 203 | + | g1t repo sync # bring your fork's default branch up to date | |
| 204 | + | ``` | |
| 205 | + | ||
| 206 | + | A repository is private unless `--public` says otherwise, and is made in | |
| 207 | + | your own workspace unless its name says `WORKSPACE/NAME`. `--source` | |
| 208 | + | adds it as a remote of a local repository (`origin` unless `--remote` | |
| 209 | + | names another) and pushes its current branch. `g1t repo fork` forks into | |
| 210 | + | your own workspace, or `--workspace`; see | |
| 211 | + | [forking a repository](/guides/forking-repositories/). | |
| 212 | + | ||
| 194 | 213 | ### Issues | |
| 195 | 214 | ||
| 196 | 215 | ```sh | |
| ⋯ | |||
| 221 | 240 | the branch's commits; `--issue` says which issue it is for; `--draft` opens | |
| 222 | 241 | it as a draft. | |
| 223 | 242 | ||
| 224 | − | To start a pull request for an issue without a branch of your own, as g1t's | |
| 225 | − | agents do, run it with `--issue` from the default branch. g1t opens a draft | |
| 226 | − | with a [working copy](/guides/pull-requests/) of its own and prints the | |
| 243 | + | To start a pull request for an issue without a branch of your own, as | |
| 244 | + | coding agents usually do, run it with `--issue` from the default branch. g1t opens a draft | |
| 245 | + | with a [working copy](/concepts/forks/#working-copies-for-pull-requests) of its own and prints the | |
| 227 | 246 | copy's git remote. Push your commits there, then mark it ready with a | |
| 228 | 247 | summary of what changed and why: | |
| 229 | 248 | ||
| ⋯ | |||
| 368 | 387 | ||
| 369 | 388 | | Command | What it does | | |
| 370 | 389 | | --- | --- | | |
| 371 | − | | `g1t session start` | Starts a session and prints its id. `--harness` (`claude_code`, `codex`, `cursor`, `copilot` or `other`; `claude-code` works too; `claude_managed_agents` is only for the sessions g1t starts itself, with `g1t work dispatch`), `--harness-session-id`, `--issue`, `--pull`, `--branch` (the current one by default) and `--title`. Starting again with the same `--harness-session-id` returns the same session. For a subagent, `--parent` (the session that started it), `--agent-type`, `--task`, `--prompt` and `--model`; a subagent's session is not kept as the working copy's (nor is one with `--no-remember`). | | |
| 390 | + | | `g1t session start` | Starts a session and prints its id. `--harness` (`claude_code`, `codex`, `cursor`, `copilot` or `other`; `claude-code` works too), `--harness-session-id`, `--issue`, `--pull`, `--branch` (the current one by default) and `--title`. Starting again with the same `--harness-session-id` returns the same session. For a subagent, `--parent` (the session that started it), `--agent-type`, `--task`, `--prompt` and `--model`; a subagent's session is not kept as the working copy's (nor is one with `--no-remember`). | | |
| 372 | 391 | | `g1t session heartbeat` | Says the session is still working, and renews its claims. A session with no heartbeat for 15 minutes reads as idle, and after 2 hours as ended. | | |
| 373 | 392 | | `g1t session log TEXT` | Records a note. `--kind` takes `note`, `prompt`, `message`, `tool_call` (or `tool-call`) or `skill` (a skill's name), `--tool` names the tool, and `--key` makes recording the same entry twice do nothing. `-` reads the text from stdin; put `--` before text that starts with a dash. | | |
| 374 | 393 | | `g1t session attach` | Points the session at an `--issue`, a `--pull`, a `--branch` or its latest `--head-commit`, or gives it a `--title`: what it is for, which replaces the title it had. | | |
| ⋯ | |||
| 433 | 452 | `--managed` fails rather than hand off. `--mode` is `plan`, `implement` or | |
| 434 | 453 | `review`. See [Start a session on your Anthropic account](/guides/sessions/#start-a-session-on-your-anthropic-account). | |
| 435 | 454 | ||
| 455 | + | `g1t issue edit 12 --add-assignee g1t` does the same as **Assign to g1t** | |
| 456 | + | on the site: g1t starts a session on the issue on the workspace's | |
| 457 | + | Anthropic account, as you. See | |
| 458 | + | [Assign g1t](/guides/sessions/#assign-g1t-or-ask-it-in-the-thread). | |
| 459 | + | ||
| 436 | 460 | ### Dependencies | |
| 437 | 461 | ||
| 438 | 462 | ```sh | |
| 314 | 314 | | Workflow run | A [workflow](/guides/actions/) run that is queued, waiting, running, or waiting for approval, in the ten projects pushed to most in the span (their newest 20 runs each) | | |
| 315 | 315 | | Build | A project's newest build, queued or building | | |
| 316 | 316 | ||
| 317 | + | ## While the page is open | |
| 318 | + | ||
| 319 | + | Home keeps up with the workspace without a reload. What lands, starts | |
| 320 | + | running, finishes or comes to need you while you are on the page joins its | |
| 321 | + | list, sliding in at its place and lit for a moment; counts and the | |
| 322 | + | sentence under the heading update with it. Runs that finish leave Running | |
| 323 | + | now. | |
| 324 | + | ||
| 325 | + | | Home updates when | From | | |
| 326 | + | | --- | --- | | |
| 327 | + | | Commits are pushed to a project's default branch, or a tag is pushed | The workspace's projects (the first 25) | | |
| 328 | + | | A pull request is opened, reviewed, merged or closed, or an issue or comment changes | The same projects | | |
| 329 | + | | A workflow run or a job in it starts or finishes, a build or deployment moves, or a release or package is published | The same projects | | |
| 330 | + | | A [session](/guides/sessions/) starts or ends | The same projects | | |
| 331 | + | | Something comes to need you, or you deal with it elsewhere | Your notifications | | |
| 332 | + | ||
| 333 | + | A busy workspace sends changes all the time, so Home refreshes a second | |
| 334 | + | after the last one and at most every five seconds. It works like the | |
| 335 | + | [pages that update live](/guides/notifications/#pages-that-update-live): | |
| 336 | + | never while you are typing in Ask or have a dialog open, and a tab out of | |
| 337 | + | view catches up when you come back to it. Pushes to other branches and a | |
| 338 | + | commit's checks don't refresh Home, because it doesn't show them. Spend | |
| 339 | + | updates with the rest but changes only as usage is metered. | |
| 340 | + | ||
| 341 | + | Refreshing in place keeps the same span: it is the same visit. | |
| 342 | + | ||
| 317 | 343 | ## Without Code access | |
| 318 | 344 | ||
| 319 | 345 | A member whose membership does not include Code sees Home without anything |
| 207 | 207 | | An **issue** | Someone comments on it or changes it, or a pull request linked to it moves. | | |
| 208 | 208 | | **Actions** and a **run** | A run starts, one of its jobs starts or finishes, or the run finishes. A running job's log streams on its own. | | |
| 209 | 209 | | **Security** | An alert is opened, fixed, dismissed or reopened. | | |
| 210 | + | | Your workspace's **Home** | Something lands, runs, deploys or comes to need you in its projects (the first 25). See [Home](/guides/home/#while-the-page-is-open). | | |
| 210 | 211 | ||
| 211 | 212 | A page refreshes a second after the last change it shows, so a burst of | |
| 212 | 213 | them is one refresh. It never refreshes while you are typing in it or have |
| 599 | 599 | git push origin my-branch | |
| 600 | 600 | ``` | |
| 601 | 601 | ||
| 602 | − | For a pull request in its own fork, clone the fork and pull `main` into | |
| 602 | + | For a pull request in its own [working copy](/concepts/forks/#working-copies-for-pull-requests), clone the working copy and pull `main` into | |
| 603 | 603 | it instead: | |
| 604 | 604 | ||
| 605 | 605 | ```sh | |
| ⋯ | |||
| 633 | 633 | is behind; otherwise it shows it when the repository suggests updating | |
| 634 | 634 | branches (on by default), and merging brings the branch up to date by | |
| 635 | 635 | itself. Whoever can push to the pull request (whoever opened it, for one | |
| 636 | − | in its own fork; anyone with the Write [role](/guides/access-and-roles/) | |
| 636 | + | in its own working copy; anyone with the Write [role](/guides/access-and-roles/) | |
| 637 | 637 | or higher, for a branch of the repository; whoever can push to the fork, | |
| 638 | 638 | for a branch of a fork) can update it, two ways: | |
| 639 | 639 | ||
| 69 | 69 | ## Private vulnerability reporting | |
| 70 | 70 | ||
| 71 | 71 | A repository takes private reports when **Private vulnerability reporting** | |
| 72 | − | is on: a switch on the Security overview's Reporting rows and in | |
| 73 | − | [Security settings](/guides/security/#settings), for Admins and security | |
| 74 | − | managers. Off by default. | |
| 72 | + | is on: a switch on the [Security overview](/guides/security/#the-overview)'s | |
| 73 | + | Reporting rows and on the Security section's **Settings** page, for Admins | |
| 74 | + | and security managers. Off by default. | |
| 75 | 75 | ||
| 76 | 76 | With it on, **Report a vulnerability** is on the repository's Security tab | |
| 77 | 77 | for anyone who can see it. Someone signed in reports under their name; on a |
| 1 | 1 | --- | |
| 2 | 2 | title: Self-hosted runners | |
| 3 | − | description: Run your workflow jobs, and if you choose g1t's checks and merge queue, on your own machines. Their time costs nothing. | |
| 3 | + | description: Run your workflow jobs, and if you choose g1t's own checks, on your own machines. Their time costs nothing. | |
| 4 | 4 | --- | |
| 5 | 5 | ||
| 6 | 6 | import { TabItem, Tabs } from '@astrojs/starlight/components'; | |
| ⋯ | |||
| 721 | 721 | ## g1t's work on your runners | |
| 722 | 722 | ||
| 723 | 723 | **Runners → Where work runs** can send g1t's own work to your runners | |
| 724 | − | too: the checks g1t runs on pull requests, merge checks and the merge | |
| 725 | − | queue's test runs. Switch on **Run g1t's work on self-hosted runners** and | |
| 724 | + | too: the checks g1t runs on pull requests and merge checks. Switch on **Run g1t's work on self-hosted runners** and | |
| 726 | 725 | give the labels a runner needs to take it (`self-hosted` is always one). | |
| 727 | 726 | ||
| 728 | 727 | - The work runs exactly as in g1t's sandbox: the same harness, with a | |
| 1 | 1 | --- | |
| 2 | 2 | title: What is g1t? | |
| 3 | − | description: g1t is the code platform your team and your own coding agents work on. Repositories, pull requests, Actions and Deployments, with Chat, Artifacts and a front page for the day, and an MCP server, API and webhooks for the agents you bring. In alpha, with every feature marked Live, Preview or Coming. | |
| 3 | + | description: g1t is the code platform for your team and the coding agents you bring. Repositories, pull requests, checks, the merge queue, Actions and Deployments; Chat for your team; and an MCP server, CLI, API and webhooks for your agents. In alpha, with every feature marked Live, Preview or Coming. | |
| 4 | 4 | tableOfContents: false | |
| 5 | 5 | prev: false | |
| 6 | 6 | --- | |
| ⋯ | |||
| 11 | 11 | import LinkCard from '../../components/LinkCard.astro'; | |
| 12 | 12 | import Status from '../../components/Status.astro'; | |
| 13 | 13 | ||
| 14 | − | g1t is the code platform your team and your own coding agents work on. | |
| 15 | 14 | **Code** holds your repositories, issues and pull requests, with required | |
| 16 | − | checks, the merge queue and Actions; **Deployments** puts every pull request | |
| 17 | − | live on a preview and ships production on merge. Your team talks in | |
| 18 | − | **Chat**, writes docs together in **Artifacts**, and **Home** tells you | |
| 19 | − | where you're needed and what happened while you were away. | |
| 15 | + | checks, the merge queue and Actions. **Deployments** puts every pull | |
| 16 | + | request live on a preview and ships production on merge. | |
| 17 | + | ||
| 18 | + | **Chat** is where your team talks: channels, direct messages and threads, | |
| 19 | + | free on every plan. **Home** shows where you're needed and what happened | |
| 20 | + | while you were away, and **Artifacts** holds the docs you write together. | |
| 20 | 21 | ||
| 21 | − | Bring the coding agents you already use, such as Claude Code, Codex, | |
| 22 | − | Cursor or OpenCode. They connect over [MCP](/guides/bring-your-own-agent/) | |
| 23 | − | and work as you: they pick up issues, open pull requests, run your checks, | |
| 24 | − | and record their sessions and the decisions behind each change. | |
| 22 | + | **Your coding agents** work here as you. Claude Code, Codex, Cursor, | |
| 23 | + | OpenCode or any MCP client connects once, then picks up issues, opens pull | |
| 24 | + | requests, waits on your checks and records how it made each change. g1t | |
| 25 | + | runs no agents of its own: the work runs in your harness, on your account. | |
| 26 | + | With an Anthropic key in the workspace, assigning an issue to `g1t` starts | |
| 27 | + | a session on that account for you. | |
| 25 | 28 | ||
| 26 | − | g1t is MIT-licensed. You can use it on g1t.sh, or run it yourself. | |
| 29 | + | g1t is MIT-licensed, and its source is at | |
| 30 | + | [g1t.sh/flagon-io/g1t](https://g1t.sh/flagon-io/g1t). Use it on g1t.sh, | |
| 31 | + | or run it yourself. | |
| 27 | 32 | ||
| 28 | 33 | <Aside type="note" title="g1t is in alpha"> | |
| 29 | − | Accounts are by invite while the alpha runs. Every feature on these pages | |
| 30 | − | is marked with where it stands: | |
| 34 | + | Anyone can sign up. Every feature on these pages is marked with where it | |
| 35 | + | stands: | |
| 31 | 36 | ||
| 32 | 37 | - <Status is="live" /> works on g1t.sh for every workspace. | |
| 33 | 38 | - <Status is="preview" /> works on g1t.sh, but is limited to some workspaces | |
| ⋯ | |||
| 42 | 47 | <LinkCard | |
| 43 | 48 | title="Quickstart" | |
| 44 | 49 | icon="rocket" | |
| 45 | − | description="Create a workspace, push a repository and connect your coding agent in a few minutes." | |
| 50 | + | description="Sign up, push a repository, and have your coding agent open a pull request you merge, in a few minutes." | |
| 46 | 51 | href="/quickstart/" | |
| 47 | 52 | /> | |
| 48 | 53 | <LinkCard | |
| ⋯ | |||
| 53 | 58 | /> | |
| 54 | 59 | <LinkCard | |
| 55 | 60 | title="Build and ship an app with an agent" | |
| 56 | − | icon="rocket" | |
| 61 | + | icon="ship" | |
| 57 | 62 | description="Your agent creates the repository, pushes the app and merges its pull requests. You turn deployments on once, and every merge ships." | |
| 58 | 63 | href="/guides/build-and-ship/" | |
| 59 | 64 | /> | |
| ⋯ | |||
| 65 | 70 | /> | |
| 66 | 71 | </CardGrid> | |
| 67 | 72 | ||
| 68 | − | ## The workspace | |
| 69 | − | ||
| 70 | − | Everything happens in a workspace: your company, or one team in it. The | |
| 71 | − | rail down the left holds Home, Chat, Notifications, Code and Artifacts, the apps you pin, and People, Workspace and your account at its | |
| 72 | − | foot. On a phone it is a bar along the bottom. | |
| 73 | − | ||
| 74 | − | | Feature | What it does | Status | | |
| 75 | − | | --- | --- | --- | | |
| 76 | − | | [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" /> | | |
| 77 | − | | [Chat](/guides/chat/) | Channels, direct messages and threads, live. No seats and no history limit. | <Status is="live" /> | | |
| 78 | − | | [Notifications](/guides/notifications/) | Everything waiting on you: mentions, reviews, approvals and alerts, with browser push. | <Status is="live" /> | | |
| 79 | − | | [People and teams](/guides/people-and-teams/) | Everyone in the workspace in one search, with profiles and teams. Members, roles, nested teams and invites. | <Status is="live" /> | | |
| 80 | − | | [Artifacts](/guides/artifacts/) | Documents written together, live, by your team and the coding agents you connect. Private until you share them. | <Status is="live" /> | | |
| 81 | − | | More kinds of artifact | Slides, designs and dashboards that keep the query behind every number. | <Status is="coming" /> | | |
| 82 | − | | Scratchpads | Tools and reports anyone can build on a copy of the data, used in production only after review. | <Status is="coming" /> | | |
| 83 | − | | [The desktop app](/guides/desktop/) | g1t in a window of its own for Windows, macOS and Linux, with notifications from your system. | <Status is="preview" /> | | |
| 84 | − | | [The phone app](/guides/mobile/) | Chat with your team, live, with notifications on the phone; review pull requests. Android first, for g1t.sh or your own g1t; not in the stores yet. | <Status is="preview" /> | | |
| 85 | − | | [A runner in the desktop app](/guides/desktop/#the-runner) | Your workspace's self-hosted jobs on your computer while g1t is open, registered and kept up to date by the app, sandboxed by default: Linux jobs in a sandbox of the runner's own on Windows, macOS and Linux, and Windows jobs in Windows Sandbox on Windows 11 Pro and up. | <Status is="preview" /> | | |
| 86 | − | ||
| 87 | 73 | ## Code and Deployments | |
| 88 | 74 | ||
| 89 | 75 | | Feature | What it does | Status | | |
| 90 | 76 | | --- | --- | --- | | |
| 91 | − | | [Repositories](/guides/managing-repositories/) and [git](/guides/git/) | Public and private repositories over HTTPS, with creating one by pushing to it. | <Status is="live" /> | | |
| 92 | − | | [Issues and pull requests](/guides/pull-requests/) | Issues, pull requests, line comments and reviews. | <Status is="live" /> | | |
| 77 | + | | [Repositories](/guides/managing-repositories/) and [git](/guides/git/) | Public and private repositories over HTTPS. Pushing to a repository that does not exist creates it, private. | <Status is="live" /> | | |
| 78 | + | | [Issues and pull requests](/guides/pull-requests/) | Issues, pull requests, line comments, reviews, merge methods and auto-merge. | <Status is="live" /> | | |
| 93 | 79 | | [Checks](/guides/checks/), [rules](/guides/rules/) and the [merge queue](/guides/merge-queue/) | Required checks, protected branches, code owners, and changes tested together before they land. | <Status is="live" /> | | |
| 94 | 80 | | [Workflows](/guides/actions/) | Workflows from `.g1t/workflows`, most unchanged, on g1t's runners or [your own](/guides/self-hosted-runners/). | <Status is="live" /> | | |
| 95 | − | | [Connect a coding agent](/guides/bring-your-own-agent/) | Any coding agent or MCP client works on g1t as you and records its session onto its pull requests. | <Status is="live" /> | | |
| 81 | + | | [Forks](/guides/forking-repositories/) | Fork a repository into your workspace, keep it in sync, and propose changes back. | <Status is="live" /> | | |
| 96 | 82 | | [Packages](/guides/packages/) | npm, Cargo, Composer, Maven, NuGet, RubyGems, Go modules and container images. | <Status is="live" /> | | |
| 97 | 83 | | [Security](/guides/security/) | Secret push protection, history scanning and pull requests that upgrade vulnerable dependencies. | <Status is="live" /> | | |
| 98 | 84 | | [Deployments](/guides/deployments/) | A live preview of every pull request on g1t.page, production on merge, and custom domains. | <Status is="live" /> | | |
| 99 | − | | Preview data | Each preview gets its own copy of the database and bucket, deleted when the pull request closes. | <Status is="coming" /> | | |
| 100 | 85 | | [Docker deployments](/guides/deployments/#docker-deployments) | Any app with a Dockerfile, or a Node.js, Python, Go, Ruby or PHP server, runs as a container that sleeps when no one visits. | <Status is="live" /> | | |
| 101 | 86 | | [Observability](/guides/observability/) | Requests, logs, traces, metrics and grouped errors, by deployment: from apps on g1t.page with nothing to set up, and from anywhere else over OpenTelemetry. | <Status is="live" /> | | |
| 87 | + | | [GitHub](/guides/github/) | Import repositories with every branch and tag, and their issues. | <Status is="live" /> | | |
| 88 | + | | Preview data | Each preview gets its own copy of the database and bucket, deleted when the pull request closes. | <Status is="coming" /> | | |
| 102 | 89 | | Rollback and always on | One click back to any deploy, and container apps kept warm. | <Status is="coming" /> | | |
| 103 | − | | [GitHub](/guides/github/) | Import repositories with every branch and tag, and their issues. | <Status is="live" /> | | |
| 104 | 90 | | Git over SSH | `git@g1t.sh` remotes. Use HTTPS until then. | <Status is="coming" /> | | |
| 105 | 91 | ||
| 92 | + | ## Coding agents | |
| 93 | + | ||
| 94 | + | | Feature | What it does | Status | | |
| 95 | + | | --- | --- | --- | | |
| 96 | + | | [Connect a coding agent](/guides/bring-your-own-agent/) | Claude Code, Codex, Cursor, OpenCode or any MCP client signs in once and works on g1t as you, with the access you give it. | <Status is="live" /> | | |
| 97 | + | | [The Claude Code plugin](/guides/claude-code-plugin/) | Records every Claude Code session in a g1t repository, gives Claude the issue's context, and adds g1t's MCP server, skills and commands. | <Status is="live" /> | | |
| 98 | + | | [The g1t CLI](/guides/cli/) | Issues, pull requests, checks, runs, workflows, secrets and logs from a terminal, a script or an agent's shell. | <Status is="live" /> | | |
| 99 | + | | [Sessions and claims](/guides/sessions/) | Who is working on what, right now: each agent's session, the issue it claimed, and the files it shares with others. | <Status is="live" /> | | |
| 100 | + | | [Assign g1t](/guides/sessions/#assign-g1t-or-ask-it-in-the-thread) | Assign an issue to `g1t`, or mention `@g1t` in a thread, to start a session on the workspace's own Anthropic account. | <Status is="live" /> | | |
| 101 | + | | [Why-blame](/guides/why-blame/) | Any line traced to its commit, pull request, issue, and the agent's own account of why it changed. | <Status is="live" /> | | |
| 102 | + | ||
| 103 | + | ## The workspace | |
| 104 | + | ||
| 105 | + | Everything happens in a workspace: your company, or one team in it. The | |
| 106 | + | rail down the left holds Home, Chat, Notifications, Code and Artifacts, | |
| 107 | + | the apps you pin, and People, Workspace and your account at its foot. On | |
| 108 | + | a phone it is a bar along the bottom. | |
| 109 | + | ||
| 110 | + | | Feature | What it does | Status | | |
| 111 | + | | --- | --- | --- | | |
| 112 | + | | [Chat](/guides/chat/) | Channels, direct messages and threads, live, and public channels anyone signed in to g1t can join. No seats and no history limit. | <Status is="live" /> | | |
| 113 | + | | [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="live" /> | | |
| 114 | + | | [Ask g1t](/guides/ask/) | Questions about your workspace, answered on the workspace's own AI provider, from Home, the Ask panel or a direct message to `@g1t`. | <Status is="live" /> | | |
| 115 | + | | [Notifications](/guides/notifications/) | Everything waiting on you: mentions, reviews, approvals and alerts, with browser push. | <Status is="live" /> | | |
| 116 | + | | [People and teams](/guides/people-and-teams/) | Everyone in the workspace in one search, with profiles and teams. Members, roles, nested teams and invites. | <Status is="live" /> | | |
| 117 | + | | [Artifacts](/guides/artifacts/) | Documents written together, live, by your team and the coding agents you connect. Private until you share them. | <Status is="live" /> | | |
| 118 | + | | [The desktop app](/guides/desktop/) | g1t in a window of its own for Windows, macOS and Linux, with notifications from your system. Unsigned during the alpha. | <Status is="preview" /> | | |
| 119 | + | | [A runner in the desktop app](/guides/desktop/#the-runner) | Your workspace's self-hosted jobs on your computer while g1t is open, sandboxed by default. Checked end to end on Windows; built the same way, not yet run, on macOS and Linux. | <Status is="preview" /> | | |
| 120 | + | | [The phone app](/guides/mobile/) | Chat with your team, live, with notifications on the phone; review pull requests. Android first, not in the stores yet. | <Status is="preview" /> | | |
| 121 | + | | More kinds of artifact | Slides, designs and dashboards that keep the query behind every number. | <Status is="coming" /> | | |
| 122 | + | ||
| 106 | 123 | ## Extend and connect | |
| 107 | 124 | ||
| 108 | 125 | | Feature | What it does | Status | | |
| 109 | 126 | | --- | --- | --- | | |
| 110 | − | | [Integrations](/guides/integrations/) | The GitHub App, Sentry, Datadog, Jira, Linear and alerts. | <Status is="live" /> | | |
| 111 | 127 | | [API](/reference/api/), [MCP](/reference/mcp/) and [webhooks](/guides/webhooks/) | Everything as a REST endpoint and an MCP tool, and a webhook for every event. | <Status is="live" /> | | |
| 128 | + | | [Integrations](/guides/integrations/) | The GitHub App, Sentry, Datadog, Jira, Linear and alerts. | <Status is="live" /> | | |
| 112 | 129 | | More integrations | Drive, Gmail, Calendar, Slack and Microsoft 365. | <Status is="coming" /> | | |
| 113 | − | | Mail | Email on your own domain, with shared inboxes. | <Status is="coming" /> | | |
| 114 | 130 | ||
| 115 | − | ## Your workspace | |
| 131 | + | ## Your account and workspace | |
| 116 | 132 | ||
| 117 | 133 | | Feature | What it does | Status | | |
| 118 | 134 | | --- | --- | --- | | |
| 119 | − | | [Accounts and sign-in](/guides/authentication/) | Invites, two-factor authentication, access tokens and OAuth apps. | <Status is="live" /> | | |
| 120 | − | | Single sign-on | SAML or OIDC and SCIM, set up by each workspace for itself. | <Status is="coming" /> | | |
| 121 | − | | [Usage and billing](/guides/usage-and-billing/) | No seats. Usage at what it costs, a spend limit, and itemised invoices. | <Status is="live" /> | | |
| 122 | − | | Storage | Databases and buckets your teams can create, at a level each team is given, with copies of production data for anything not yet reviewed. | <Status is="coming" /> | | |
| 135 | + | | [Accounts and sign-in](/guides/authentication/) | Open sign-up with an email address or GitHub, two-factor authentication, access tokens and OAuth apps. | <Status is="live" /> | | |
| 136 | + | | [Usage and billing](/guides/usage-and-billing/) | No seats. The forge and Chat are free; compute at what it costs, a spend limit, and itemised invoices. | <Status is="live" /> | | |
| 123 | 137 | | [Audit log](/guides/audit-log/) | Every change to the workspace, with export. | <Status is="live" /> | | |
| 124 | − | | [Run g1t yourself](/guides/self-hosting/) | The core forge in Docker. Deployments are off in this version. | <Status is="preview" /> | | |
| 138 | + | | [Run g1t yourself](/guides/self-hosting/) | The core forge in Docker. Hosted runners, deployments and billing are off in this version. | <Status is="preview" /> | | |
| 125 | 139 | | [Architecture](/architecture/overview/) | How g1t is built, service by service, for developers building on it, contributing to it or running it. | <Status is="live" /> | | |
| 140 | + | | Single sign-on | SAML or OIDC and SCIM, set up by each workspace for itself. | <Status is="coming" /> | | |
| 126 | 141 | ||
| 127 | 142 | ## Bring a repository | |
| 128 | 143 | ||
| 1 | 1 | --- | |
| 2 | 2 | title: Quickstart | |
| 3 | − | description: Create a workspace, push a repository, open an issue and have your own coding agent work on it. Then talk to your team in Chat. | |
| 3 | + | description: Sign up, push a repository, open an issue, and have your own coding agent open a pull request that you merge. Then bring your team into Chat. | |
| 4 | 4 | --- | |
| 5 | 5 | ||
| 6 | 6 | import { Steps, Tabs, TabItem } from '@astrojs/starlight/components'; | |
| 7 | 7 | import Aside from '../../components/Aside.astro'; | |
| 8 | 8 | import AgentSetup from '../../components/AgentSetup.astro'; | |
| 9 | 9 | ||
| 10 | − | A g1t workspace holds your repositories, your team's conversations and | |
| 11 | − | everything you ship. This page gets you from sign-up to a pull request your | |
| 12 | − | own coding agent opened and you merged, in a few minutes. | |
| 10 | + | This page takes you from sign-up to a pull request your coding agent | |
| 11 | + | opened and you merged. It takes a few minutes. | |
| 13 | 12 | ||
| 14 | 13 | <Aside type="tip" title="Let an assistant do it"> | |
| 15 | 14 | Give your AI assistant [g1t.sh/llms.txt](https://g1t.sh/llms.txt) and ask it | |
| ⋯ | |||
| 17 | 16 | and approve it. It never sees your password. | |
| 18 | 17 | </Aside> | |
| 19 | 18 | ||
| 20 | − | ## Put your coding agent on your code | |
| 19 | + | <Steps> | |
| 21 | 20 | ||
| 22 | − | <Steps> | |
| 21 | + | 1. **Sign up** | |
| 23 | 22 | ||
| 24 | − | 1. **Create an account and a workspace** | |
| 23 | + | [Sign up](https://g1t.sh/register) with an email address and a | |
| 24 | + | password, or with GitHub. Anyone can; no invite is needed. Confirm your | |
| 25 | + | email with the code g1t sends (or the link in the same email). | |
| 25 | 26 | ||
| 26 | − | [Sign up](https://g1t.sh/register) (or open the invite link someone sent | |
| 27 | − | you; see [invites](/guides/authentication/#invites)) and confirm your | |
| 28 | − | email with the code g1t sends it (or the link in the same email). Then create a | |
| 29 | − | **workspace**: it holds your repositories and your channels. See | |
| 27 | + | Your account comes with a **workspace** of its own, named after your | |
| 28 | + | username. It holds your repositories and your channels. See | |
| 30 | 29 | [workspaces](/guides/workspaces/). | |
| 31 | − | ||
| 32 | − | 2. **Create an access token** | |
| 33 | 30 | ||
| 34 | − | Git and the API authenticate with a token. Create one in | |
| 35 | − | [Settings → Access tokens](https://g1t.sh/settings/tokens) and copy it: it is shown once. | |
| 31 | + | 2. **Install the CLI and sign in** | |
| 36 | 32 | ||
| 37 | 33 | <Tabs syncKey="os"> | |
| 38 | 34 | <TabItem label="macOS and Linux"> | |
| 39 | 35 | ```sh | |
| 40 | − | export G1T_TOKEN=g1t_… | |
| 36 | + | curl -fsSL https://g1t.sh/install | sh | |
| 41 | 37 | ``` | |
| 42 | 38 | </TabItem> | |
| 43 | 39 | <TabItem label="Windows"> | |
| 44 | 40 | ```powershell | |
| 45 | − | $env:G1T_TOKEN = "g1t_…" | |
| 41 | + | irm https://g1t.sh/install.ps1 | iex | |
| 46 | 42 | ``` | |
| 47 | 43 | </TabItem> | |
| 48 | 44 | </Tabs> | |
| 49 | 45 | ||
| 46 | + | ```sh | |
| 47 | + | g1t auth login --git | |
| 48 | + | ``` | |
| 49 | + | ||
| 50 | + | `g1t` shows a code and opens your browser: check the code and approve. | |
| 51 | + | `--git` also sets git up to use the same sign-in for `g1t.sh`, so | |
| 52 | + | `git push` asks for nothing. `g1t auth status` shows who you are signed | |
| 53 | + | in as. See [the g1t CLI](/guides/cli/). | |
| 54 | + | ||
| 55 | + | <Aside title="Without the CLI"> | |
| 56 | + | Git and the API also take an [access token](/guides/authentication/#access-tokens): | |
| 57 | + | create one in [Settings → Access tokens](https://g1t.sh/settings/tokens), | |
| 58 | + | and give it to git as the password, with your username. | |
| 59 | + | </Aside> | |
| 60 | + | ||
| 50 | 61 | 3. **Push a repository** | |
| 51 | 62 | ||
| 52 | − | Pushing to a repository that does not exist creates it, as a **private** | |
| 53 | − | repository; make it public later if you want to. When git asks, give it your username and, as the password, | |
| 54 | − | your token. | |
| 63 | + | From a git repository with at least one commit: | |
| 64 | + | ||
| 65 | + | ```sh | |
| 66 | + | g1t repo create my-project --source . --remote g1t | |
| 67 | + | ``` | |
| 68 | + | ||
| 69 | + | This makes `<workspace>/my-project`, private, adds it as the remote | |
| 70 | + | `g1t` and pushes. Plain git works too: pushing to a repository that does | |
| 71 | + | not exist creates it, private. | |
| 55 | 72 | ||
| 56 | 73 | ```sh | |
| 57 | 74 | git remote add g1t https://g1t.sh/<workspace>/my-project.git | |
| 58 | 75 | git push -u g1t main | |
| 59 | 76 | ``` | |
| 60 | 77 | ||
| 61 | − | For a private repository, create it first with **New** in the header, | |
| 62 | − | which also makes one empty, or imported from GitHub or any git host by | |
| 63 | − | its address. | |
| 78 | + | **New** in the site's header makes one empty, or imports one from | |
| 79 | + | GitHub or any git host by its address. | |
| 64 | 80 | ||
| 65 | 81 | <Aside title="Coming from GitHub?"> | |
| 66 | 82 | Rename `.github` to `.g1t` before you push, and your GitHub Actions | |
| 67 | − | workflows run on g1t, most without changes. See | |
| 68 | − | [GitHub Actions](/guides/actions/), and | |
| 83 | + | workflows run on g1t, most without changes. Workflows are compute: on a | |
| 84 | + | free workspace, an owner checks a card once under **Settings → Billing | |
| 85 | + | and plans** to start a $5 trial, which is never charged for the check. | |
| 86 | + | See [GitHub Actions](/guides/actions/), | |
| 87 | + | [No card, no compute](/guides/usage-and-billing/#no-card-no-compute) and | |
| 69 | 88 | [what is not supported yet](/about/limitations/#actions-and-runners). | |
| 70 | 89 | </Aside> | |
| 71 | 90 | ||
| 72 | 91 | 4. **Open an issue** | |
| 73 | 92 | ||
| 74 | − | On the repository's **Issues** page, choose **New issue**. Say what is | |
| 75 | − | wrong or wanted in the title and description; your agent works from that. | |
| 76 | − | Say what done means, if you know it. What a pull request must pass to | |
| 77 | − | merge is your workflows' checks: see | |
| 78 | − | [required status checks](/guides/pull-requests/#required-status-checks). | |
| 93 | + | On the repository's **Issues** page, choose **New issue**, or: | |
| 94 | + | ||
| 95 | + | ```sh | |
| 96 | + | g1t issue create -t "Greeting should name the caller" -b "Take a name from the first argument." | |
| 97 | + | ``` | |
| 98 | + | ||
| 99 | + | Say what is wrong or wanted, and what done means if you know it. Your | |
| 100 | + | agent works from this text. | |
| 79 | 101 | ||
| 80 | 102 | 5. **Connect your coding agent** | |
| 81 | 103 | ||
| 82 | − | Connect Claude Code, Codex, Cursor, OpenCode or any MCP client to g1t: | |
| 83 | − | ||
| 84 | 104 | <AgentSetup /> | |
| 85 | 105 | ||
| 86 | 106 | Approve in your browser when it asks. Then ask: | |
| ⋯ | |||
| 88 | 108 | > Work on issue 1 of `<workspace>/my-project` on g1t. Open a pull request | |
| 89 | 109 | > for it and record your session as you go. | |
| 90 | 110 | ||
| 91 | − | It opens a pull request, pushes its change, and your workflows run on it | |
| 92 | − | as on anyone's. See [Connect a coding agent](/guides/bring-your-own-agent/). | |
| 111 | + | It opens a pull request, pushes its change, and your | |
| 112 | + | workflows run on it as on anyone's. See | |
| 113 | + | [Connect a coding agent](/guides/bring-your-own-agent/). | |
| 93 | 114 | ||
| 94 | − | 6. **Merge** | |
| 115 | + | <Aside title="Or assign it to g1t"> | |
| 116 | + | With an Anthropic key under the workspace's [**AI**](/guides/ai/) | |
| 117 | + | settings, **Assign to g1t** in the issue's sidebar starts a session on | |
| 118 | + | that account, as you, and its card shows in the issue's conversation. | |
| 119 | + | See [Assign g1t](/guides/sessions/#assign-g1t-or-ask-it-in-the-thread). | |
| 120 | + | </Aside> | |
| 95 | 121 | ||
| 96 | − | Open the pull request, read **Changes** and **Session**, and choose | |
| 97 | − | **Merge**, or turn on auto-merge in the repository's settings and let g1t | |
| 98 | − | land what passes. The issue closes, recording which pull request resolved | |
| 99 | − | it. | |
| 122 | + | 6. **Review and merge** | |
| 100 | 123 | ||
| 124 | + | Open the pull request. **Files changed** shows what it did, and | |
| 125 | + | **Session** shows how: its prompts, reasoning and tool calls. Choose | |
| 126 | + | **Merge**, or turn on **auto-merge** to have it land once its required | |
| 127 | + | checks pass. The issue closes, recording which pull request resolved it. | |
| 128 | + | ||
| 101 | 129 | </Steps> | |
| 102 | 130 | ||
| 103 | − | ## Talk to your team | |
| 131 | + | ## Bring your team | |
| 104 | 132 | ||
| 105 | − | Choose **Chat** in the rail, or go to `g1t.sh/<workspace>/-/chat`. Every | |
| 106 | − | workspace starts with `#general`. Chat is free on every plan. See | |
| 133 | + | Choose **Chat** in the rail. Every workspace starts with `#general`, and | |
| 134 | + | Chat is free on every plan. Adding people to a workspace comes with | |
| 135 | + | [the g1t plan](/guides/usage-and-billing/#the-g1t-plan); on a free one, | |
| 136 | + | [invite a friend](/guides/authentication/#invite-a-friend) to g1t instead, | |
| 137 | + | or open a [public channel](/guides/chat/#public-channels) that anyone signed in to g1t can join. See | |
| 107 | 138 | [Chat](/guides/chat/). | |
| 108 | 139 | ||
| 109 | 140 | ## Next | |
| 110 | 141 | ||
| 142 | + | - [Build and ship an app with an agent](/guides/build-and-ship/): from an empty folder to a deployed app. | |
| 111 | 143 | - [How g1t works](/concepts/overview/): issues, pull requests, checks, review and merging. | |
| 112 | 144 | - [Connect a coding agent](/guides/bring-your-own-agent/): access, repository instructions and recording sessions. | |
| 113 | − | - [GitHub Actions](/guides/actions/): your workflows, unchanged. | |
| 145 | + | - [GitHub Actions](/guides/actions/): your workflows, most unchanged. | |
| 114 | 146 | - [MCP tools](/reference/mcp/) and the [API](/reference/api/): everything above, from code. | |
| 54 | 54 | return slug.startsWith('reference/') ? TABS[TABS.length - 1] : tabForGroup(group.label ?? ''); | |
| 55 | 55 | } | |
| 56 | 56 | ||
| 57 | + | /** | |
| 58 | + | * A tab's groups in the tab's own order (`groups` in tabs.ts), so a tab | |
| 59 | + | * opens on, and its sidebar starts with, the group it is named for: Code | |
| 60 | + | * on Landing changes, not on Security, which comes first in the sidebar. | |
| 61 | + | * The API reference tab names no groups and keeps the sidebar's order. | |
| 62 | + | */ | |
| 63 | + | function inTabOrder(groups: Entry[], tab: Tab): Entry[] { | |
| 64 | + | const rank = (group: Entry) => { | |
| 65 | + | const index = tab.groups.indexOf(group.label ?? ''); | |
| 66 | + | return index === -1 ? tab.groups.length : index; | |
| 67 | + | }; | |
| 68 | + | return groups.filter((group) => tabOf(group) === tab).sort((a, b) => rank(a) - rank(b)); | |
| 69 | + | } | |
| 70 | + | ||
| 57 | 71 | /** Whether the current page is in these entries, at any depth. */ | |
| 58 | 72 | function holdsCurrent(entries: Entry[]): boolean { | |
| 59 | 73 | return entries.some((entry) => entry.isCurrent || holdsCurrent(entry.entries ?? [])); | |
| ⋯ | |||
| 74 | 88 | const tab = holder ? tabOf(holder) : TABS[0]; | |
| 75 | 89 | // A tab opens on the first page of its first group; the first tab, on the home page. | |
| 76 | 90 | const tabs = TABS.map((each) => { | |
| 77 | − | const group = each === TABS[0] ? undefined : groups.find((entry) => tabOf(entry) === each); | |
| 91 | + | const group = each === TABS[0] ? undefined : inTabOrder(groups, each)[0]; | |
| 78 | 92 | const slug = group ? firstSlug(group.entries ?? []) : undefined; | |
| 79 | 93 | return { label: each.label, href: slug === undefined ? '/' : `/${slug}/`, current: each === tab }; | |
| 80 | 94 | }); | |
| 81 | 95 | NAV.set(route, { tabs, section: section ?? (route.id === '' ? undefined : tab.label) }); | |
| 82 | − | route.sidebar = route.sidebar.filter((entry) => entry.type === 'group' && tabOf(entry as Entry) === tab); | |
| 96 | + | route.sidebar = inTabOrder(groups, tab) as typeof route.sidebar; | |
| 83 | 97 | ||
| 84 | 98 | const { title, description } = route.entry.data; | |
| 85 | 99 | const card = new URL('/docs', OG); | |
| 280 | 280 | display: block; | |
| 281 | 281 | overflow-x: auto; | |
| 282 | 282 | } | |
| 283 | + | /* A shadow at an edge with more columns past it, so a cut-off column | |
| 284 | + | reads as "scroll for more". The covers scroll with the table and hide | |
| 285 | + | the shadows at its ends. */ | |
| 286 | + | .sl-markdown-content table:not(:has(.status)) { | |
| 287 | + | --cover: var(--g1t-bg); | |
| 288 | + | /* On the dark page a shadow is a faint light, as a dark one would not show. */ | |
| 289 | + | --shade: rgb(255 255 255 / 0.12); | |
| 290 | + | background: | |
| 291 | + | linear-gradient(to right, var(--cover) 30%, transparent) left / 2.5rem 100% no-repeat local, | |
| 292 | + | linear-gradient(to left, var(--cover) 30%, transparent) right / 2.5rem 100% no-repeat local, | |
| 293 | + | radial-gradient(farthest-side at 0 50%, var(--shade), transparent) left / 0.875rem 100% no-repeat scroll, | |
| 294 | + | radial-gradient(farthest-side at 100% 50%, var(--shade), transparent) right / 0.875rem 100% no-repeat scroll; | |
| 295 | + | } | |
| 296 | + | :root[data-theme='light'] .sl-markdown-content table:not(:has(.status)) { | |
| 297 | + | --shade: rgb(24 24 27 / 0.16); | |
| 298 | + | } | |
| 283 | 299 | } | |
| 284 | 300 | /* | |
| 285 | 301 | * There, a name, route or value in a table is never broken, not even at a | |
| ⋯ | |||
| 345 | 361 | ||
| 346 | 362 | /* --- On this page --------------------------------------------------------- */ | |
| 347 | 363 | ||
| 364 | + | /* On a phone the list drops over the page: give it an edge and a shadow, so | |
| 365 | + | where it stops is clear rather than its last row running into the text. */ | |
| 366 | + | mobile-starlight-toc .dropdown { | |
| 367 | + | border-color: var(--g1t-line); | |
| 368 | + | border-bottom-color: var(--g1t-line-strong); | |
| 369 | + | box-shadow: var(--g1t-docs-menu-shadow); | |
| 370 | + | } | |
| 371 | + | ||
| 348 | 372 | .right-sidebar-panel { | |
| 349 | 373 | padding-top: 2.75rem; | |
| 350 | 374 | } | |
| 66 | 66 | // 45 minutes, then every 6 hours). Empty sends none. | |
| 67 | 67 | "STATUS_ALERT_EMAIL": "hey@flagon.io", | |
| 68 | 68 | "STATUS_FROM": "g1t status <noreply@g1t.sh>", | |
| 69 | − | "OG_IMAGE": "https://og.g1t.sh/image?path=%2Fstatus&v=2" | |
| 69 | + | "OG_IMAGE": "https://og.g1t.sh/image?path=%2Fstatus&v=3" | |
| 70 | 70 | }, | |
| 71 | 71 | // Asking for a subscription sends an email: limited per client address | |
| 72 | 72 | // and per email address (src/index.ts). Ids and limits: RATE_LIMITS in |
| 60 | 60 | --color-danger: light-dark(var(--g1t-light-danger), var(--g1t-danger)); | |
| 61 | 61 | ||
| 62 | 62 | --animate-fade-up: fade-up 0.6s ease-out both; | |
| 63 | + | /* A row that arrived on a live page (Home): in from above, lit for a moment. */ | |
| 64 | + | --animate-arrive: g1t-arrive 0.42s cubic-bezier(0.16, 1, 0.3, 1), g1t-fresh 2s ease-out; | |
| 63 | 65 | --animate-dash: dash 1.2s linear infinite; | |
| 64 | 66 | ||
| 65 | 67 | /* Menus, popovers and dialogs (components/ui): in quickly, out quicker. */ | |
| ⋯ | |||
| 138 | 140 | } | |
| 139 | 141 | } | |
| 140 | 142 | ||
| 141 | − | /* A row that just arrived on a live page (Observability's logs). */ | |
| 143 | + | /* A row that just arrived on a live page (Observability's logs, Home). */ | |
| 142 | 144 | @keyframes g1t-fresh { | |
| 143 | 145 | from { | |
| 144 | 146 | background-color: color-mix(in srgb, var(--color-accent) 18%, transparent); | |
| 145 | 147 | } | |
| 146 | 148 | } | |
| 147 | 149 | ||
| 150 | + | @keyframes g1t-arrive { | |
| 151 | + | from { | |
| 152 | + | opacity: 0; | |
| 153 | + | transform: translateY(-8px); | |
| 154 | + | } | |
| 155 | + | } | |
| 156 | + | ||
| 148 | 157 | @keyframes fade-in { | |
| 149 | 158 | from { | |
| 150 | 159 | opacity: 0; | |
| 1 | − | import { Ban, Check, ChevronRight, CircleAlert, Copy, ExternalLink, LoaderCircle, Play, SquareTerminal } from "lucide-react"; | |
| 1 | + | import { Ban, Check, ChevronRight, CircleAlert, Copy, ExternalLink, LoaderCircle, Play, Rocket, SquareTerminal } from "lucide-react"; | |
| 2 | 2 | import { useEffect, useState } from "react"; | |
| 3 | 3 | ||
| 4 | 4 | import type { AskSource } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | − | import { type Proposal, type Step, foldAnswer, proposalTitle } from "../lib/ask"; | |
| 6 | + | import { type Proposal, type Step, foldAnswer, proposalSteps, proposalTitle } from "../lib/ask"; | |
| 7 | 7 | import { Badge } from "./ui/badge"; | |
| 8 | 8 | import { Button } from "./ui/button"; | |
| 9 | 9 | import { Card } from "./ui/card"; | |
| ⋯ | |||
| 116 | 116 | ); | |
| 117 | 117 | } | |
| 118 | 118 | ||
| 119 | − | type Started = { session_url: string | null; provider_session_url: string | null; handoff: boolean; command: string | null; prompt: string }; | |
| 119 | + | type Started = { | |
| 120 | + | session_url: string | null; | |
| 121 | + | provider_session_url: string | null; | |
| 122 | + | handoff: boolean; | |
| 123 | + | command: string | null; | |
| 124 | + | prompt: string; | |
| 125 | + | /** A new repository's proposal: what the card made first. */ | |
| 126 | + | repo_url?: string; | |
| 127 | + | production_url?: string | null; | |
| 128 | + | deploy_error?: string | null; | |
| 129 | + | }; | |
| 120 | 130 | ||
| 121 | 131 | /** | |
| 122 | 132 | * Work g1t proposed to hand to a session in the person's own harness. Ask | |
| ⋯ | |||
| 130 | 140 | const [started, setStarted] = useState<Started | null>(null); | |
| 131 | 141 | const [copied, setCopied] = useState(false); | |
| 132 | 142 | const [error, setError] = useState<string | null>(null); | |
| 143 | + | const [made, setMade] = useState<Pick<Started, "repo_url" | "production_url" | "deploy_error"> | null>(null); | |
| 133 | 144 | ||
| 134 | 145 | useEffect(() => { | |
| 135 | 146 | let live = true; | |
| ⋯ | |||
| 152 | 163 | body: JSON.stringify({ ...proposal, handoff }), | |
| 153 | 164 | }); | |
| 154 | 165 | const body = (await response.json().catch(() => null)) as (Started & { error?: string }) | null; | |
| 166 | + | // The repository may be made even when the session is not: keep where it is. | |
| 167 | + | if (body?.repo_url) setMade({ repo_url: body.repo_url, production_url: body.production_url ?? null, deploy_error: body.deploy_error ?? null }); | |
| 155 | 168 | if (!response.ok || !body) throw new Error(body?.error ?? "The session could not be started."); | |
| 156 | 169 | setStarted(body); | |
| 157 | 170 | if (handoff && body.command) { | |
| ⋯ | |||
| 176 | 189 | <span className="truncate text-sm font-medium text-fg">{proposalTitle(proposal)}</span> | |
| 177 | 190 | <Badge className="px-1.5 py-0 font-normal">Session</Badge> | |
| 178 | 191 | </div> | |
| 179 | − | <p className="mt-0.5 text-xs text-muted"> | |
| 180 | − | {proposal.issue ? "Runs in your own harness, with your skills, tied to the issue." : "Opens the issue, then runs in your own harness, tied to it."} | |
| 181 | − | </p> | |
| 192 | + | <p className="mt-0.5 text-xs text-muted">{proposalSteps(proposal)}</p> | |
| 182 | 193 | {proposal.instruction && <p className="mt-1 line-clamp-2 text-xs text-faint">{proposal.instruction}</p>} | |
| 183 | 194 | </div> | |
| 184 | 195 | </div> | |
| 196 | + | {made && ( | |
| 197 | + | <div className="flex flex-col gap-1 text-xs"> | |
| 198 | + | <span className="flex flex-wrap items-center gap-1.5"> | |
| 199 | + | <Check size={13} aria-hidden="true" className="text-success" /> | |
| 200 | + | <span className="text-muted">Created</span> | |
| 201 | + | <Button asChild variant="link" size="inline" className="text-xs"> | |
| 202 | + | <a href={made.repo_url}>{proposal.repo}</a> | |
| 203 | + | </Button> | |
| 204 | + | </span> | |
| 205 | + | {made.production_url && ( | |
| 206 | + | <span className="flex flex-wrap items-center gap-1.5"> | |
| 207 | + | <Rocket size={13} aria-hidden="true" className="text-accent" /> | |
| 208 | + | <span className="text-muted">Live once the first version is pushed at</span> | |
| 209 | + | <Button asChild variant="link" size="inline" className="text-xs"> | |
| 210 | + | <a href={made.production_url} target="_blank" rel="noreferrer"> | |
| 211 | + | {made.production_url.replace(/^https?:\/\//, "")} <ExternalLink size={11} aria-hidden="true" /> | |
| 212 | + | </a> | |
| 213 | + | </Button> | |
| 214 | + | </span> | |
| 215 | + | )} | |
| 216 | + | {made.deploy_error && ( | |
| 217 | + | <span className="flex items-start gap-1.5 text-warn"> | |
| 218 | + | <CircleAlert size={12} aria-hidden="true" className="mt-0.5 shrink-0" /> | |
| 219 | + | Not deployed: {made.deploy_error} | |
| 220 | + | </span> | |
| 221 | + | )} | |
| 222 | + | </div> | |
| 223 | + | )} | |
| 185 | 224 | {running ? ( | |
| 186 | 225 | <div className="flex flex-wrap items-center gap-2 text-xs"> | |
| 187 | 226 | <Check size={13} aria-hidden="true" className="text-success" /> | |
| 78 | 78 | { | |
| 79 | 79 | title: "Product", | |
| 80 | 80 | links: [ | |
| 81 | − | ["Explore", "/explore"], | |
| 82 | − | ["Pricing", "/pricing"], | |
| 83 | − | ["Download", "/download"], | |
| 81 | + | // Code first, as on the landing page: then Chat, then the agents. | |
| 82 | + | ["Pull requests", "https://docs.g1t.sh/guides/pull-requests/"], | |
| 83 | + | ["Deployments", "https://docs.g1t.sh/guides/deployments/"], | |
| 84 | 84 | ["Chat", "https://docs.g1t.sh/guides/chat/"], | |
| 85 | 85 | ["Bring your own agent", "https://docs.g1t.sh/guides/bring-your-own-agent/"], | |
| 86 | 86 | ["Integrations", "https://docs.g1t.sh/guides/integrations/"], | |
| 87 | + | ["Explore", "/explore"], | |
| 88 | + | ["Pricing", "/pricing"], | |
| 89 | + | ["Download", "/download"], | |
| 87 | 90 | ], | |
| 88 | 91 | }, | |
| 89 | 92 | { |
| 2 | 2 | * Home's parts (routes/workspace/home.tsx): Where you're needed, Start | |
| 3 | 3 | * here, what happened in the span (what people did, what landed, builds, | |
| 4 | 4 | * spend) and Running now. Each takes what | |
| 5 | − | * lib/home.ts worked out and only draws it. | |
| 5 | + | * lib/home.ts worked out and only draws it; a row that arrived while the | |
| 6 | + | * page was open comes in (`animate-arrive`, lib/use-home-live.ts). | |
| 6 | 7 | */ | |
| 7 | 8 | import { | |
| 8 | 9 | AlertTriangle, | |
| ⋯ | |||
| 53 | 54 | whenShort, | |
| 54 | 55 | } from "../lib/home"; | |
| 55 | 56 | import { askG1t } from "../lib/ask"; | |
| 57 | + | import { useArrived } from "../lib/use-home-live"; | |
| 56 | 58 | import { cn } from "../lib/cn"; | |
| 57 | 59 | import { money } from "../lib/money"; | |
| 58 | 60 | import { CommitText } from "./commit-message"; | |
| ⋯ | |||
| 165 | 167 | /** Rows shown before the rest fold away. */ | |
| 166 | 168 | const ROWS = 8; | |
| 167 | 169 | ||
| 168 | − | function NeedItem({ row, now }: { row: AttentionRow; now: number }) { | |
| 170 | + | /** A row that arrived while the page was open: in from above, lit for a moment. */ | |
| 171 | + | const ARRIVED = "animate-arrive motion-reduce:animate-none"; | |
| 172 | + | ||
| 173 | + | function NeedItem({ row, now, arrived }: { row: AttentionRow; now: number; arrived?: boolean }) { | |
| 169 | 174 | return ( | |
| 170 | − | <li className="flex items-start gap-3 border-t border-line px-5 py-3 first:border-t-0"> | |
| 175 | + | <li className={cn("flex items-start gap-3 border-t border-line px-5 py-3 first:border-t-0", arrived && ARRIVED)}> | |
| 171 | 176 | <span className="mt-0.5 shrink-0">{ICON[row.kind]}</span> | |
| 172 | 177 | <div className="min-w-0 grow"> | |
| 173 | 178 | <Link to={row.action.to} className="line-clamp-2 text-sm font-semibold text-fg hover:underline sm:block sm:truncate"> | |
| ⋯ | |||
| 202 | 207 | /** Everything waiting on you, most pressing and longest waiting first; past the first few, the rest fold away. */ | |
| 203 | 208 | export function NeedsList({ attention, now }: { attention: Attention; now: number }) { | |
| 204 | 209 | const { rows, missing } = attention; | |
| 210 | + | const arrived = useArrived(rows.map((row) => row.key)); | |
| 205 | 211 | return ( | |
| 206 | 212 | <> | |
| 207 | 213 | {rows.length === 0 ? ( | |
| ⋯ | |||
| 209 | 215 | ) : ( | |
| 210 | 216 | <ul> | |
| 211 | 217 | {rows.slice(0, ROWS).map((row) => ( | |
| 212 | − | <NeedItem key={row.key} row={row} now={now} /> | |
| 218 | + | <NeedItem key={row.key} row={row} now={now} arrived={arrived.has(row.key)} /> | |
| 213 | 219 | ))} | |
| 214 | 220 | </ul> | |
| 215 | 221 | )} | |
| ⋯ | |||
| 221 | 227 | </summary> | |
| 222 | 228 | <ul className="border-t border-line"> | |
| 223 | 229 | {rows.slice(ROWS).map((row) => ( | |
| 224 | − | <NeedItem key={row.key} row={row} now={now} /> | |
| 230 | + | <NeedItem key={row.key} row={row} now={now} arrived={arrived.has(row.key)} /> | |
| 225 | 231 | ))} | |
| 226 | 232 | </ul> | |
| 227 | 233 | </details> | |
| ⋯ | |||
| 565 | 571 | ); | |
| 566 | 572 | } | |
| 567 | 573 | ||
| 568 | − | function LandedItem({ change, now, tz, timeOnly }: { change: LandedChange; now: number; tz: string | null; timeOnly?: boolean }) { | |
| 574 | + | function LandedItem({ change, now, tz, timeOnly, arrived }: { change: LandedChange; now: number; tz: string | null; timeOnly?: boolean; arrived?: boolean }) { | |
| 569 | 575 | return ( | |
| 570 | − | <li className={cn(rowClass, "items-start")}> | |
| 576 | + | <li className={cn(rowClass, "items-start", arrived && ARRIVED)}> | |
| 571 | 577 | <span className="mt-0.5 shrink-0">{LANDED_ICON[change.kind]}</span> | |
| 572 | 578 | <div className="min-w-0 grow"> | |
| 573 | 579 | <div className="flex min-w-0 items-center gap-1.5"> | |
| ⋯ | |||
| 608 | 614 | now, | |
| 609 | 615 | tz, | |
| 610 | 616 | byDay = false, | |
| 617 | + | scope, | |
| 611 | 618 | }: { | |
| 612 | 619 | landed: LandedChange[] | null; | |
| 613 | 620 | missing: string[]; | |
| ⋯ | |||
| 615 | 622 | now: number; | |
| 616 | 623 | tz: string | null; | |
| 617 | 624 | byDay?: boolean; | |
| 625 | + | /** The span: switching it lists other things, which are not arrivals. */ | |
| 626 | + | scope?: string; | |
| 618 | 627 | }) { | |
| 628 | + | const arrived = useArrived(landed?.map((change) => change.key) ?? [], scope); | |
| 619 | 629 | if (!code) return null; | |
| 620 | − | const render = (change: LandedChange) => <LandedItem key={change.key} change={change} now={now} tz={tz} timeOnly={byDay} />; | |
| 630 | + | const render = (change: LandedChange) => <LandedItem key={change.key} change={change} now={now} tz={tz} timeOnly={byDay} arrived={arrived.has(change.key)} />; | |
| 621 | 631 | return ( | |
| 622 | 632 | <Card title="Landed" hint={landed && landed.length > 0 ? landedHint(landed) : undefined}> | |
| 623 | 633 | {!landed ? ( | |
| ⋯ | |||
| 713 | 723 | ); | |
| 714 | 724 | } | |
| 715 | 725 | ||
| 716 | − | export function DeploysCard({ deploys, complete, code, now, tz }: { deploys: DeployRow[] | null; complete: boolean; code: boolean; now: number; tz: string | null }) { | |
| 726 | + | export function DeploysCard({ | |
| 727 | + | deploys, | |
| 728 | + | complete, | |
| 729 | + | code, | |
| 730 | + | now, | |
| 731 | + | tz, | |
| 732 | + | scope, | |
| 733 | + | }: { | |
| 734 | + | deploys: DeployRow[] | null; | |
| 735 | + | complete: boolean; | |
| 736 | + | code: boolean; | |
| 737 | + | now: number; | |
| 738 | + | tz: string | null; | |
| 739 | + | scope?: string; | |
| 740 | + | }) { | |
| 741 | + | const arrived = useArrived(deploys?.map((row) => row.key) ?? [], scope); | |
| 717 | 742 | // A workspace that doesn't deploy has nothing to say here. | |
| 718 | 743 | if (!code || (deploys && deploys.length === 0)) return null; | |
| 719 | 744 | const failed = deploys?.filter((row) => row.outcome === "failed").length ?? 0; | |
| ⋯ | |||
| 725 | 750 | <Folded | |
| 726 | 751 | items={deploys} | |
| 727 | 752 | render={(row) => ( | |
| 728 | − | <li key={row.key} className={rowClass}> | |
| 753 | + | <li key={row.key} className={cn(rowClass, arrived.has(row.key) && ARRIVED)}> | |
| 729 | 754 | <Rocket size={15} className={cn("shrink-0", row.outcome === "failed" ? "text-danger" : "text-success")} /> | |
| 730 | 755 | <div className="min-w-0 grow"> | |
| 731 | 756 | <Link to={row.to} className="block truncate text-sm text-fg hover:underline"> | |
| ⋯ | |||
| 823 | 848 | }; | |
| 824 | 849 | ||
| 825 | 850 | export function RunningList({ rows, missing, now }: { rows: RunningRow[]; missing: string[]; now: number }) { | |
| 851 | + | const arrived = useArrived(rows.map((row) => row.key)); | |
| 826 | 852 | return ( | |
| 827 | 853 | <> | |
| 828 | 854 | {rows.length === 0 ? ( | |
| ⋯ | |||
| 831 | 857 | <Folded | |
| 832 | 858 | items={rows} | |
| 833 | 859 | render={(row) => ( | |
| 834 | − | <li key={row.key} className={rowClass}> | |
| 860 | + | <li key={row.key} className={cn(rowClass, arrived.has(row.key) && ARRIVED)}> | |
| 835 | 861 | <span className="shrink-0">{RUNNING_ICON[row.kind]}</span> | |
| 836 | 862 | <div className="min-w-0 grow"> | |
| 837 | 863 | <Link to={row.to} className="block truncate text-sm text-fg hover:underline"> | |
| 70 | 70 | /** A session g1t proposed in an answer: the person starts it, or copies its command. */ | |
| 71 | 71 | export type Proposal = Extract<AskEvent, { type: "proposal" }>; | |
| 72 | 72 | ||
| 73 | + | /** What starting a proposal does, in a line, under its title. */ | |
| 74 | + | export function proposalSteps(proposal: Proposal): string { | |
| 75 | + | if (proposal.new_repository) { | |
| 76 | + | const made = `Creates ${proposal.private ? "the private" : "the public"} repository`; | |
| 77 | + | return proposal.deploy | |
| 78 | + | ? `${made}, turns on its deployments, then runs in your own harness and pushes the first version, which goes live on g1t.page.` | |
| 79 | + | : `${made}, then runs in your own harness and pushes the first version.`; | |
| 80 | + | } | |
| 81 | + | return proposal.issue ? "Runs in your own harness, with your skills, tied to the issue." : "Opens the issue, then runs in your own harness, tied to it."; | |
| 82 | + | } | |
| 83 | + | ||
| 73 | 84 | /** What a proposal asks the session to do, in a line: "Implement #12 in acme/web". */ | |
| 74 | 85 | export function proposalTitle(proposal: Proposal): string { | |
| 86 | + | if (proposal.new_repository) return `Build “${proposal.title}” in ${proposal.repo}`; | |
| 75 | 87 | const verb = { plan: "Plan", implement: "Implement", review: "Review" }[proposal.mode] ?? "Work on"; | |
| 76 | 88 | return proposal.issue ? `${verb} #${proposal.issue} in ${proposal.repo}` : `${verb} “${proposal.title}” in ${proposal.repo}`; | |
| 77 | 89 | } |
| 1 | + | import assert from "node:assert/strict"; | |
| 2 | + | import { describe, it } from "node:test"; | |
| 3 | + | ||
| 4 | + | import type { RepoChange } from "@g1t/contracts"; | |
| 5 | + | ||
| 6 | + | import { arrivedOf, homeShows, type HomeRepo } from "./home-live.ts"; | |
| 7 | + | ||
| 8 | + | const repos: HomeRepo[] = [{ id: "r1", path: "acme/web", defaultBranch: "main" }]; | |
| 9 | + | const change = (kind: string, extra: Partial<RepoChange> = {}): RepoChange => ({ repo_id: "r1", kind, at: "2026-10-15T00:00:00Z", ...extra }); | |
| 10 | + | ||
| 11 | + | describe("homeShows", () => { | |
| 12 | + | it("takes a default branch's pushes and tags, not another branch's", () => { | |
| 13 | + | assert.equal(homeShows(change("git.push", { ref: "refs/heads/main" }), repos), true); | |
| 14 | + | assert.equal(homeShows(change("git.push", { ref: "refs/tags/v1.0.0" }), repos), true); | |
| 15 | + | assert.equal(homeShows(change("git.push", { ref: "refs/heads/feature" }), repos), false); | |
| 16 | + | }); | |
| 17 | + | ||
| 18 | + | it("takes any branch's push where the default branch is not known", () => { | |
| 19 | + | assert.equal(homeShows({ ...change("git.push", { ref: "refs/heads/feature" }), repo_id: "other" }, repos), true); | |
| 20 | + | }); | |
| 21 | + | ||
| 22 | + | it("takes what Home lists and leaves a commit's checks", () => { | |
| 23 | + | for (const kind of ["pull.merged", "review.submitted", "issue.opened", "comment.created", "workflow.completed", "run.updated", "deployment_status.created", "release.published", "session.started", "session.ended"]) { | |
| 24 | + | assert.equal(homeShows(change(kind), repos), true, kind); | |
| 25 | + | } | |
| 26 | + | for (const kind of ["status.created", "check_run.completed", "check_suite.completed", "checks.completed", "session.progressed", "git.deleted"]) { | |
| 27 | + | assert.equal(homeShows(change(kind), repos), false, kind); | |
| 28 | + | } | |
| 29 | + | }); | |
| 30 | + | }); | |
| 31 | + | ||
| 32 | + | describe("arrivedOf", () => { | |
| 33 | + | it("finds nothing new on the first draw", () => { | |
| 34 | + | const { arrived, seen } = arrivedOf(null, "last", ["a", "b"]); | |
| 35 | + | assert.deepEqual([...arrived], []); | |
| 36 | + | assert.deepEqual([...seen.keys], ["a", "b"]); | |
| 37 | + | }); | |
| 38 | + | ||
| 39 | + | it("finds the rows that were not shown before", () => { | |
| 40 | + | const first = arrivedOf(null, "last", ["a", "b"]).seen; | |
| 41 | + | const { arrived } = arrivedOf(first, "last", ["c", "a", "b"]); | |
| 42 | + | assert.deepEqual([...arrived], ["c"]); | |
| 43 | + | }); | |
| 44 | + | ||
| 45 | + | it("does not count a row that went and came back as new", () => { | |
| 46 | + | const first = arrivedOf(null, "last", ["a", "b"]).seen; | |
| 47 | + | const second = arrivedOf(first, "last", ["b"]).seen; | |
| 48 | + | assert.deepEqual([...arrivedOf(second, "last", ["a", "b"]).arrived], []); | |
| 49 | + | }); | |
| 50 | + | ||
| 51 | + | it("starts over when the span switches", () => { | |
| 52 | + | const first = arrivedOf(null, "last", ["a"]).seen; | |
| 53 | + | assert.deepEqual([...arrivedOf(first, "7d", ["a", "b", "c"]).arrived], []); | |
| 54 | + | }); | |
| 55 | + | }); |
| 1 | + | /** | |
| 2 | + | * Home that updates live: which of its repositories' changes it shows, | |
| 3 | + | * and which rows are new since it last drew. Pure; the hook that uses | |
| 4 | + | * them is lib/use-home-live.ts. | |
| 5 | + | * | |
| 6 | + | * Home watches the workspace's projects (the first HOME_WATCH_MAX, as the | |
| 7 | + | * digest reads them) over the live socket, as a repository page | |
| 8 | + | * watches its own (lib/repo-live.ts), and refreshes when something it | |
| 9 | + | * lists moves: a push to a default branch, a pull request or review, an | |
| 10 | + | * issue or comment, a run starting or finishing, a deployment or release, | |
| 11 | + | * a session starting or ending. Not a branch's pushes or a commit's | |
| 12 | + | * checks: a busy workspace sends those all the time, and nothing on Home | |
| 13 | + | * shows them. Its loader reads many services, so it refreshes at most | |
| 14 | + | * every HOME_GAP_MS. | |
| 15 | + | */ | |
| 16 | + | import type { RepoChange } from "@g1t/contracts"; | |
| 17 | + | ||
| 18 | + | import { branchOf } from "./repo-live.ts"; | |
| 19 | + | ||
| 20 | + | /** The most projects Home watches (a person watches at most 50, other tabs' pages too). */ | |
| 21 | + | export const HOME_WATCH_MAX = 25; | |
| 22 | + | /** However many changes arrive, Home refreshes at most this often. */ | |
| 23 | + | export const HOME_GAP_MS = 5000; | |
| 24 | + | ||
| 25 | + | /** A project Home watches: its id, `owner/name` and default branch. */ | |
| 26 | + | export type HomeRepo = { id: string; path: string; defaultBranch: string | null }; | |
| 27 | + | ||
| 28 | + | const FAMILIES = ["pull", "review", "issue", "comment", "workflow", "run", "deployment", "deployment_status", "release", "package"]; | |
| 29 | + | const KINDS = ["session.started", "session.ended", "queue.changed"]; | |
| 30 | + | ||
| 31 | + | /** Whether a change moves something Home shows. */ | |
| 32 | + | export function homeShows(change: RepoChange, repos: readonly HomeRepo[]): boolean { | |
| 33 | + | if (change.kind === "git.push") { | |
| 34 | + | const repo = repos.find((one) => one.id === change.repo_id); | |
| 35 | + | const branch = branchOf(change.ref); | |
| 36 | + | // A default branch's push lands; a tag's is a release. | |
| 37 | + | return branch == null || !repo?.defaultBranch || branch === repo.defaultBranch; | |
| 38 | + | } | |
| 39 | + | return FAMILIES.includes(change.kind.split(".")[0] ?? "") || KINDS.includes(change.kind); | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | /** What a list on the page remembers between draws: the keys it has shown, under which scope. */ | |
| 43 | + | export type Seen = { scope: string; keys: ReadonlySet<string> }; | |
| 44 | + | ||
| 45 | + | /** | |
| 46 | + | * The rows of `keys` that were not on the list when it last drew, and | |
| 47 | + | * what to remember now. On a first draw, or once the scope changed (the | |
| 48 | + | * span switched), none are new: only what arrives while it is open is. | |
| 49 | + | */ | |
| 50 | + | export function arrivedOf(seen: Seen | null, scope: string, keys: readonly string[]): { arrived: ReadonlySet<string>; seen: Seen } { | |
| 51 | + | if (!seen || seen.scope !== scope) return { arrived: new Set(), seen: { scope, keys: new Set(keys) } }; | |
| 52 | + | const arrived = new Set(keys.filter((key) => !seen.keys.has(key))); | |
| 53 | + | return { arrived, seen: { scope, keys: new Set([...seen.keys, ...keys]) } }; | |
| 54 | + | } |
| 229 | 229 | busy: boolean; | |
| 230 | 230 | /** A navigation or a submission is under way: the router reloads the page itself. */ | |
| 231 | 231 | navigating: boolean; | |
| 232 | + | /** The page refreshes at most this often; LIVE_MIN_GAP_MS unless a page's loader is heavier (Home's). */ | |
| 233 | + | gap?: number; | |
| 232 | 234 | }; | |
| 233 | 235 | ||
| 234 | 236 | /** | |
| ⋯ | |||
| 251 | 253 | const quiet = input.now - (state.lastChangeAt ?? state.pendingSince); | |
| 252 | 254 | const waited = input.now - state.pendingSince; | |
| 253 | 255 | const since = state.lastRunAt == null ? Infinity : input.now - state.lastRunAt; | |
| 254 | − | const ms = Math.max(quiet < LIVE_DEBOUNCE_MS && waited < LIVE_MAX_WAIT_MS ? Math.min(LIVE_DEBOUNCE_MS - quiet, LIVE_MAX_WAIT_MS - waited) : 0, LIVE_MIN_GAP_MS - since); | |
| 256 | + | const ms = Math.max(quiet < LIVE_DEBOUNCE_MS && waited < LIVE_MAX_WAIT_MS ? Math.min(LIVE_DEBOUNCE_MS - quiet, LIVE_MAX_WAIT_MS - waited) : 0, (input.gap ?? LIVE_MIN_GAP_MS) - since); | |
| 255 | 257 | return ms > 0 ? { action: "wait", ms } : { action: "run" }; | |
| 256 | 258 | } | |
| 257 | 259 | ||
| 1 | + | /** | |
| 2 | + | * `useHomeLive`: Home refreshing itself as the workspace moves | |
| 3 | + | * (lib/home-live.ts says what it shows), and `useArrived`: which rows of | |
| 4 | + | * a list on it are new, so they come in rather than appear. | |
| 5 | + | * | |
| 6 | + | * Besides its projects' changes, Home refreshes when the inbox moves or a | |
| 7 | + | * notification for this workspace arrives: Where you're needed reads them. | |
| 8 | + | */ | |
| 9 | + | import { useEffect, useRef } from "react"; | |
| 10 | + | ||
| 11 | + | import { HOME_GAP_MS, type HomeRepo, type Seen, arrivedOf, homeShows } from "./home-live"; | |
| 12 | + | import { useNotifyState } from "./notify-client"; | |
| 13 | + | import { useLive } from "./use-repo-live"; | |
| 14 | + | ||
| 15 | + | export function useHomeLive(slug: string, repos: readonly HomeRepo[]): boolean { | |
| 16 | + | const { live, nudge } = useLive({ repos, signedIn: true, match: (change) => homeShows(change, repos), gap: HOME_GAP_MS }); | |
| 17 | + | ||
| 18 | + | const { inbox, recent } = useNotifyState(); | |
| 19 | + | const newest = recent.find((n) => n.workspace.toLowerCase() === slug)?.id ?? null; | |
| 20 | + | const before = useRef<{ inbox: number | null; newest: string | null } | null>(null); | |
| 21 | + | useEffect(() => { | |
| 22 | + | const was = before.current; | |
| 23 | + | before.current = { inbox, newest }; | |
| 24 | + | // The first the feed says is where the page started, not a change. | |
| 25 | + | if (was && ((was.inbox != null && inbox !== was.inbox) || newest !== was.newest)) nudge(); | |
| 26 | + | }, [inbox, newest, nudge]); | |
| 27 | + | ||
| 28 | + | return live; | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | /** | |
| 32 | + | * The keys of a list's rows that were not on it when it last drew: none | |
| 33 | + | * on the first draw, nor right after `scope` (the span) changes. The same | |
| 34 | + | * keys again give the same answer, so a row keeps its entrance for as | |
| 35 | + | * long as it plays. | |
| 36 | + | */ | |
| 37 | + | export function useArrived(keys: readonly string[], scope = ""): ReadonlySet<string> { | |
| 38 | + | const last = useRef<{ seen: Seen | null; id: string; arrived: ReadonlySet<string> }>({ seen: null, id: "", arrived: new Set() }); | |
| 39 | + | const id = `${scope}\n${keys.join("\n")}`; | |
| 40 | + | if (last.current.seen == null || id !== last.current.id) { | |
| 41 | + | const next = arrivedOf(last.current.seen, scope, keys); | |
| 42 | + | last.current = { seen: next.seen, id, arrived: next.arrived }; | |
| 43 | + | } | |
| 44 | + | return last.current.arrived; | |
| 45 | + | } |
| 13 | 13 | * socket is down, the page refreshes when the tab comes back into view | |
| 14 | 14 | * (at most every 30 s) and once a minute while it is in view. | |
| 15 | 15 | * | |
| 16 | + | * `useLive` is the same for a page over several repositories (Home, | |
| 17 | + | * the workspace's): one `watch_repos` asks for them all. | |
| 18 | + | * | |
| 16 | 19 | * Browser-only work happens in effects; on the server the hook does | |
| 17 | 20 | * nothing. | |
| 18 | 21 | */ | |
| ⋯ | |||
| 60 | 63 | show: () => void; | |
| 61 | 64 | }; | |
| 62 | 65 | ||
| 66 | + | /** A repository a live page watches: its id, as changes name it, and its `owner/name`, as it is asked for. */ | |
| 67 | + | export type WatchedRepo = { id: string; path: string }; | |
| 68 | + | ||
| 69 | + | export type LiveOptions = RepoLiveOptions & { | |
| 70 | + | /** The repositories whose changes the page hears. */ | |
| 71 | + | repos: readonly WatchedRepo[]; | |
| 72 | + | /** Whether the viewer is signed in: only then is there a socket to hear on. */ | |
| 73 | + | signedIn: boolean; | |
| 74 | + | /** However many changes arrive, the page refreshes at most this often (LIVE_MIN_GAP_MS). */ | |
| 75 | + | gap?: number; | |
| 76 | + | }; | |
| 77 | + | ||
| 78 | + | export type Live = RepoLive & { | |
| 79 | + | /** Something outside the repositories changed (the inbox, on Home): refresh as a change would. */ | |
| 80 | + | nudge: () => void; | |
| 81 | + | }; | |
| 82 | + | ||
| 63 | 83 | // ── Watching ───────────────────────────────────────────────────────────── | |
| 64 | 84 | ||
| 65 | 85 | /** When each repository was last asked for, by `owner/name`, so pages and reconnects ask once. */ | |
| ⋯ | |||
| 67 | 87 | /** Asking again sooner than this is the same ask. */ | |
| 68 | 88 | const ASK_AGAIN_MS = 5000; | |
| 69 | 89 | ||
| 70 | − | async function watchRepo(path: string, force: boolean): Promise<boolean> { | |
| 90 | + | /** Asks to hear `paths`, one request for them all; true when any is watched. */ | |
| 91 | + | async function watchRepos(paths: readonly string[], force: boolean): Promise<boolean> { | |
| 71 | 92 | const now = Date.now(); | |
| 72 | − | const last = asked.get(path); | |
| 73 | − | if (!force && last != null && now - last < ASK_AGAIN_MS) return true; | |
| 74 | − | asked.set(path, now); | |
| 93 | + | const due = paths.filter((path) => { | |
| 94 | + | const last = asked.get(path); | |
| 95 | + | return force || last == null || now - last >= ASK_AGAIN_MS; | |
| 96 | + | }); | |
| 97 | + | if (due.length === 0) return true; | |
| 98 | + | for (const path of due) asked.set(path, now); | |
| 75 | 99 | try { | |
| 76 | 100 | const answer = await fetch("/-/notify", { | |
| 77 | 101 | method: "POST", | |
| 78 | 102 | headers: { "content-type": "application/json", accept: "application/json" }, | |
| 79 | 103 | credentials: "same-origin", | |
| 80 | − | body: JSON.stringify({ intent: "watch_repo", repo: path }), | |
| 104 | + | body: JSON.stringify(due.length === 1 ? { intent: "watch_repo", repo: due[0] } : { intent: "watch_repos", repos: due }), | |
| 81 | 105 | }); | |
| 82 | 106 | if (!answer.ok) throw new Error(`status ${answer.status}`); | |
| 83 | 107 | const body = (await answer.json().catch(() => null)) as { ok?: unknown } | null; | |
| 84 | 108 | if (body?.ok !== true) throw new Error("refused"); | |
| 85 | 109 | return true; | |
| 86 | 110 | } catch { | |
| 87 | − | asked.delete(path); | |
| 111 | + | for (const path of due) asked.delete(path); | |
| 88 | 112 | return false; | |
| 89 | 113 | } | |
| 90 | 114 | } | |
| ⋯ | |||
| 111 | 135 | export function useRepoLive(options: RepoLiveOptions): RepoLive { | |
| 112 | 136 | const layout = useRouteLoaderData("routes/repo/layout") as LayoutData | undefined; | |
| 113 | 137 | const repo = layout?.repo ?? null; | |
| 114 | − | const signedIn = layout?.signedIn === true; | |
| 115 | − | const enabled = options.enabled !== false && !!repo; | |
| 116 | − | const path = repo ? `${repo.namespace}/${repo.name}` : null; | |
| 117 | − | const repoId = repo?.id ?? null; | |
| 138 | + | const repos = repo ? [{ id: repo.id, path: `${repo.namespace}/${repo.name}` }] : []; | |
| 139 | + | const { live, held, show } = useLive({ ...options, enabled: options.enabled !== false && !!repo, repos, signedIn: layout?.signedIn === true }); | |
| 140 | + | return { live, held, show }; | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | /** | |
| 144 | + | * A page that refreshes itself as any of `repos` changes (Home watches | |
| 145 | + | * the workspace's): `useRepoLive` for a page with no repository layout. | |
| 146 | + | */ | |
| 147 | + | export function useLive(options: LiveOptions): Live { | |
| 148 | + | const { repos, signedIn } = options; | |
| 149 | + | // With no repositories there is nothing to hear: the page refreshes as it would without the socket. | |
| 150 | + | const enabled = options.enabled !== false; | |
| 151 | + | // One string for the effects: the same repositories are the same watch. | |
| 152 | + | const paths = repos.map((repo) => repo.path).join(" "); | |
| 153 | + | const ids = repos.map((repo) => repo.id).join(" "); | |
| 118 | 154 | ||
| 119 | 155 | const connected = useFeedConnected(); | |
| 120 | 156 | const [watching, setWatching] = useState(false); | |
| ⋯ | |||
| 145 | 181 | const check = useCallback(() => { | |
| 146 | 182 | if (timer.current) clearTimeout(timer.current); | |
| 147 | 183 | timer.current = null; | |
| 148 | − | const decision = gate(state.current, { now: Date.now(), visible: visibleNow(), busy: busyNow(), navigating: latest.current.navigating }); | |
| 184 | + | const decision = gate(state.current, { now: Date.now(), visible: visibleNow(), busy: busyNow(), navigating: latest.current.navigating, gap: latest.current.options.gap }); | |
| 149 | 185 | if (decision.action === "run") run(); | |
| 150 | 186 | else if (decision.action === "wait") timer.current = setTimeout(check, decision.ms); | |
| 151 | 187 | // "hold": out of view; coming back into view (`back` below) asks again. | |
| ⋯ | |||
| 162 | 198 | [check], | |
| 163 | 199 | ); | |
| 164 | 200 | ||
| 165 | − | // Watching the repository: on opening the page, whenever the socket | |
| 201 | + | // Watching the repositories: on opening the page, whenever the socket | |
| 166 | 202 | // (re)connects, and before the watch runs out. | |
| 167 | 203 | useEffect(() => { | |
| 168 | − | if (!enabled || !signedIn || !connected || !path) { | |
| 204 | + | if (!enabled || !signedIn || !connected || !paths) { | |
| 169 | 205 | setWatching(false); | |
| 170 | 206 | return; | |
| 171 | 207 | } | |
| 172 | 208 | let cancelled = false; | |
| 173 | 209 | const ask = (force: boolean) => | |
| 174 | − | void watchRepo(path, force).then((ok) => { | |
| 210 | + | void watchRepos(paths.split(" "), force).then((ok) => { | |
| 175 | 211 | if (!cancelled) setWatching(ok); | |
| 176 | 212 | }); | |
| 177 | 213 | ask(true); | |
| ⋯ | |||
| 180 | 216 | cancelled = true; | |
| 181 | 217 | clearInterval(renew); | |
| 182 | 218 | }; | |
| 183 | − | }, [enabled, signedIn, connected, path]); | |
| 219 | + | }, [enabled, signedIn, connected, paths]); | |
| 184 | 220 | ||
| 185 | 221 | // Changes as they arrive. | |
| 186 | 222 | useEffect(() => { | |
| 187 | − | if (!enabled || !repoId) return; | |
| 223 | + | if (!enabled || !ids) return; | |
| 224 | + | const watched = new Set(ids.split(" ")); | |
| 188 | 225 | return onRepoChanges((changes) => { | |
| 189 | 226 | const { match, hold } = latest.current.options; | |
| 190 | − | const mine = changes.filter((change) => change.repo_id === repoId); | |
| 227 | + | const mine = changes.filter((change) => watched.has(change.repo_id)); | |
| 191 | 228 | const holding = hold ? mine.filter((change) => hold(change)) : []; | |
| 192 | 229 | const matched = mine.filter((change) => !holding.includes(change) && match(change)); | |
| 193 | 230 | if (holding.length) setHeld((before) => [...before, ...holding]); | |
| 194 | 231 | // While something is held, the rest waits with it: shown together. | |
| 195 | 232 | if (matched.length && !holding.length && heldCount.current === 0) queue(); | |
| 196 | 233 | }); | |
| 197 | − | }, [enabled, repoId, queue]); | |
| 234 | + | }, [enabled, ids, queue]); | |
| 198 | 235 | ||
| 199 | 236 | // Another branch or page: what was held is no longer about it. | |
| 200 | 237 | useEffect(() => { | |
| ⋯ | |||
| 245 | 282 | run(); | |
| 246 | 283 | }, [run]); | |
| 247 | 284 | ||
| 248 | − | return { live, held, show }; | |
| 285 | + | const nudge = useCallback(() => { | |
| 286 | + | if (enabled && heldCount.current === 0) queue(); | |
| 287 | + | }, [enabled, queue]); | |
| 288 | + | ||
| 289 | + | return { live, held, show, nudge }; | |
| 249 | 290 | } | |
| 18 | 18 | ...page(args, { | |
| 19 | 19 | title: "g1t · Where your team's code lands", | |
| 20 | 20 | description: | |
| 21 | − | "Repositories, pull requests, required checks, a merge queue and deployments, with chat for your team. Connect the coding agents you already use through MCP and the API. Chat is free on every plan, never per seat.", | |
| 21 | + | // At most 200 characters, or a preview cuts it short (lib/meta.ts). | |
| 22 | + | "Repositories, pull requests, required checks, a merge queue and deployments, for your team and the coding agents you bring over MCP and the API. Chat is free on every plan. Never per seat.", | |
| 22 | 23 | }), | |
| 23 | 24 | organization(), | |
| 24 | 25 | application(20), |
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Comments
Sign in to comment.