Merge branch 'worktree-agent-ac950b9c891220ad3'
42 files+104−6840/42 viewed
| 14 | 14 | | --- | --- | | |
| 15 | 15 | | Something a person does on g1t.sh | The guide for it in `apps/docs/src/content/docs/guides/` | | |
| 16 | 16 | | An API route, a field, or an MCP tool | `apps/api/src/operations.rs` descriptions (they feed the OpenAPI document and the API reference), the example and notes for it in `apps/api/src/reference.json`, `reference/mcp.md`, and any guide that shows the call. Then refresh the docs' copy of the OpenAPI document with `G1T_WRITE_OPENAPI=1 cargo test -p g1t-api openapi`; `cargo test` fails until you do. | | |
| 17 | − | | How agents behave | `guides/working-with-g1t.md`, and `apps/web/public/llms.txt` | | |
| 17 | + | | What coding agents are told about g1t | `guides/bring-your-own-agent.mdx`, `guides/claude-code-plugin.md`, the plugin's skills in `plugins/claude-code/`, and `apps/web/public/llms.txt` | | |
| 18 | 18 | | Settings, limits or prices | The page that names them, and the table it is in | | |
| 19 | 19 | | A new feature | A section in the guide that owns it, linked from the docs home if it is a new task | | |
| 20 | 20 | | How g1t is put together: a service, a binding, a store, an event route, the path of a request | Its page in the docs' Architecture section (`apps/docs/src/content/docs/architecture/`), and its diagram in `apps/docs/src/diagrams/architecture.ts`. [How g1t ships](https://docs.g1t.sh/architecture/shipping/#keeping-this-section-true) says which page covers what. | | |
| ⋯ | |||
| 61 | 61 | - Hover hints use the `Tooltip` component, never `title=`. Native | |
| 62 | 62 | `<select>`, file inputs, checkboxes and radios use their `ui` | |
| 63 | 63 | components. | |
| 64 | − | - **Every avatar is `Avatar`** (`ui/avatar.tsx`), and every agent's is | |
| 65 | − | `Avatar` with `agent` set, or `AgentAvatar` (`components/agent-avatar.tsx`), | |
| 66 | − | which wraps it. That draws the agent's bot face (`AgentFace`, | |
| 67 | − | `components/agent-face.tsx`) from its `look` or its `avatar_seed`, puts | |
| 68 | − | the agent marker in its corner and names it "…, agent" to a screen | |
| 69 | − | reader. Never hand-draw an agent's face, a sparkle, a letter on a tile or | |
| 70 | − | the marker at a call site: a place that shows an agent differently is a | |
| 71 | − | bug. | |
| 64 | + | - **Every avatar is `Avatar`** (`ui/avatar.tsx`), and @g1t's is `Avatar` | |
| 65 | + | with `agent` set, or `AgentAvatar` (`components/agent-avatar.tsx`), which | |
| 66 | + | wraps it: g1t's pixel 1 with the agent marker in its corner, named | |
| 67 | + | "…, agent" to a screen reader. Never hand-draw a sparkle, a letter on a | |
| 68 | + | tile or the marker at a call site: a place that shows @g1t differently is | |
| 69 | + | a bug. | |
| 72 | 70 | - Look at every page you change, in light and dark, on a desktop and a | |
| 73 | 71 | phone, before calling it done. | |
| 74 | 72 | ||
| ⋯ | |||
| 337 | 335 | Worker's secret `GIT_FALLBACK_NAMESPACES`) and open an incident. To | |
| 338 | 336 | switch back, `reconcile` anything the fallback took, then delete the | |
| 339 | 337 | secret. The script's header has every step. | |
| 340 | − | - **Costs and margin**, model prices, credits and the platform pause are | |
| 338 | + | - **Costs and margin**, credits and the platform pause are | |
| 341 | 339 | in sudo, under **Costs & margin**. The code is in `services/billing/src` | |
| 342 | 340 | (`costs.rs`, `margin.rs`, `pricing.rs`, `budget.rs`, `platform.rs`). | |
| 234 | 234 | pub kind: String, | |
| 235 | 235 | pub name: String, | |
| 236 | 236 | pub repo: String, | |
| 237 | − | /// The repository's Actions (or Agents) page, where its run is. | |
| 237 | + | /// The repository's Actions page, where its run is. | |
| 238 | 238 | pub link: Option<String>, | |
| 239 | 239 | } | |
| 240 | 240 |
| 70 | 70 | ## Coming soon | |
| 71 | 71 | ||
| 72 | 72 | Every category has integrations on the way, among them Slack, Microsoft | |
| 73 | − | Teams, Notion, Confluence, PagerDuty, Cloudflare, AWS, Figma, Okta and | |
| 74 | − | Google Drive for workspaces. For you: **Google Calendar** and **Outlook | |
| 73 | + | Teams, Google Docs, PagerDuty, AWS, Miro, Okta and Google Drive for | |
| 74 | + | workspaces. For you: **Google Calendar** and **Outlook | |
| 75 | 75 | Calendar** will set your [status](/guides/chat/#presence-and-status) to *In a meeting* while | |
| 76 | 76 | you are in one; **Zoom** | |
| 77 | 77 | and **Google Meet** will set it to *On a call*. A status you set by hand |
| 186 | 186 | ||
| 187 | 187 | /** | |
| 188 | 188 | * A person's or an agent's picture: the uploaded one when there is one, | |
| 189 | − | * else their initial. Agents carry a marker, as everywhere on g1t. The | |
| 190 | − | * site draws an agent's own bot face (components/agent-face.tsx); bringing | |
| 191 | − | * that here is to come. | |
| 189 | + | * else their initial. Agents (@g1t, and authors of old messages from | |
| 190 | + | * retired agents) carry a marker, as everywhere on g1t. | |
| 192 | 191 | */ | |
| 193 | 192 | export function Avatar({ name, uri, agent, size = 32 }: { name: string; uri?: string | null; agent?: boolean; size?: number }) { | |
| 194 | 193 | const colors = useColors(); |
| 19 | 19 | DOCS_URL: string; | |
| 20 | 20 | /** Where deployed apps live: `https://g1t.page`. */ | |
| 21 | 21 | PAGES_URL: string; | |
| 22 | − | MODELS_URL: string; | |
| 23 | 22 | /** A public repository that always exists, `owner/name`, to fetch refs of. */ | |
| 24 | 23 | PROBE_REPO: string; | |
| 25 | 24 | /** The site as browsers reach it, when the checks reach it another way. */ | |
| ⋯ | |||
| 91 | 90 | const mcp = trim(vars.MCP_URL); | |
| 92 | 91 | const docs = trim(vars.DOCS_URL); | |
| 93 | 92 | const pages = trim(vars.PAGES_URL); | |
| 94 | − | const models = trim(vars.MODELS_URL); | |
| 95 | 93 | const repo = (vars.PROBE_REPO ?? "").trim(); | |
| 96 | 94 | // What the page shows as the site's address: where people reach it. | |
| 97 | 95 | const shown = host(trim(vars.PUBLIC_SITE_URL) || site); | |
| ⋯ | |||
| 186 | 184 | check: { kind: "http", steps: [{ url: `${pages}/` }] }, | |
| 187 | 185 | } | |
| 188 | 186 | : null, | |
| 189 | − | models | |
| 190 | − | ? { | |
| 191 | − | key: "agents", | |
| 192 | − | name: "Agents' model proxy", | |
| 193 | − | address: host(models), | |
| 194 | − | checks: "The proxy agents reach their model through. The model providers behind it are not checked.", | |
| 195 | − | core: false, | |
| 196 | − | check: { kind: "http", steps: [{ url: `${models}/` }] }, | |
| 197 | − | } | |
| 198 | − | : null, | |
| 199 | − | models | |
| 187 | + | pages | |
| 200 | 188 | ? { | |
| 201 | 189 | key: "sandboxes", | |
| 202 | 190 | name: "Sandboxes", | |
| 203 | − | address: "Agents, checks, workflows and builds", | |
| 191 | + | address: "Checks, workflows and builds", | |
| 204 | 192 | checks: "Starting a sandbox costs money and takes seconds, so it is not checked from here yet.", | |
| 205 | 193 | core: false, | |
| 206 | 194 | check: { kind: "none" }, | |
| 25 | 25 | MCP_URL: "https://mcp.g1t.sh", | |
| 26 | 26 | DOCS_URL: "https://docs.g1t.sh", | |
| 27 | 27 | PAGES_URL: "https://g1t.page", | |
| 28 | − | MODELS_URL: "https://models.g1t.sh", | |
| 29 | 28 | PROBE_REPO: "flagon-io/g1t", | |
| 30 | 29 | }; | |
| 31 | 30 | ||
| ⋯ | |||
| 188 | 187 | const all = components(VARS, true); | |
| 189 | 188 | assert.deepEqual( | |
| 190 | 189 | all.map((c) => c.key), | |
| 191 | − | ["site", "api", "git", "speed", "mcp", "docs", "deployments", "agents", "sandboxes", "billing"], | |
| 190 | + | ["site", "api", "git", "speed", "mcp", "docs", "deployments", "sandboxes", "billing"], | |
| 192 | 191 | ); | |
| 193 | 192 | const speed = all.find((c) => c.key === "speed")!; | |
| 194 | 193 | assert.deepEqual(speed.check, { kind: "http", steps: [{ url: "https://g1t.sh/flagon-io/g1t", browser: true }, { url: "https://g1t.sh/explore", browser: true }] }); | |
| 28 | 28 | SITE_URL: "https://g1t.sh", | |
| 29 | 29 | API_URL: "https://api.g1t.sh", | |
| 30 | 30 | DOCS_URL: "https://docs.g1t.sh", | |
| 31 | − | MODELS_URL: "https://models.g1t.sh", | |
| 31 | + | PAGES_URL: "https://g1t.page", | |
| 32 | 32 | PROBE_REPO: "flagon-io/g1t", | |
| 33 | 33 | }, | |
| 34 | 34 | false, | |
| ⋯ | |||
| 75 | 75 | }); | |
| 76 | 76 | ||
| 77 | 77 | test("the page: banner, every part with its bar, and no incidents", () => { | |
| 78 | − | const html = renderPage(page({ site: "up", api: "up", git: "up", docs: "up", agents: "up" }), OPTIONS); | |
| 78 | + | const html = renderPage(page({ site: "up", api: "up", git: "up", docs: "up", deployments: "up" }), OPTIONS); | |
| 79 | 79 | assert.match(html, /<h1>All systems normal<\/h1>/); | |
| 80 | 80 | for (const name of ["Website and sign-in", "API", "Git and repositories", "Documentation", "Sandboxes"]) assert.ok(html.includes(name), name); | |
| 81 | 81 | assert.equal((html.match(/<ol class="bar"/g) ?? []).length, 5, "Sandboxes has no bar"); | |
| ⋯ | |||
| 89 | 89 | ||
| 90 | 90 | test("an outage and an open incident are said at the top, escaped", () => { | |
| 91 | 91 | const html = renderPage( | |
| 92 | − | page({ site: "down", api: "up", git: "up", docs: "up", agents: "up" }, [ | |
| 92 | + | page({ site: "down", api: "up", git: "up", docs: "up" }, [ | |
| 93 | 93 | INCIDENT, | |
| 94 | 94 | ]), | |
| 95 | 95 | OPTIONS, | |
| 353 | 353 | const past = incidents.filter((i) => i.resolved_at != null && Date.parse(i.resolved_at) >= recentSince); | |
| 354 | 354 | const inProgress = maintenance.filter((m) => maintenanceState(m, options.now) === "in_progress"); | |
| 355 | 355 | const upcoming = maintenance.filter((m) => maintenanceState(m, options.now) === "scheduled"); | |
| 356 | − | const description = `${overall.title}. Live status of g1t: the website, API, git, MCP, docs, deployments, agents and billing, with 90 days of history.`; | |
| 356 | + | const description = `${overall.title}. Live status of g1t: the website, API, git, MCP, docs, deployments, sandboxes and billing, with 90 days of history.`; | |
| 357 | 357 | const checkedLine = checked_at | |
| 358 | 358 | ? stale | |
| 359 | 359 | ? `The checks have not run since ${time(checked_at, true)}, so what follows may be out of date.` |
| 53 | 53 | "MCP_URL": "https://mcp.g1t.sh", | |
| 54 | 54 | "DOCS_URL": "https://docs.g1t.sh", | |
| 55 | 55 | "PAGES_URL": "https://g1t.page", | |
| 56 | − | "MODELS_URL": "https://models.g1t.sh", | |
| 57 | 56 | // A public repository that always exists, whose branches are listed | |
| 58 | 57 | // as a clone would. | |
| 59 | 58 | "PROBE_REPO": "flagon-io/g1t", |
| 179 | 179 | register or rename a workspace to while it exists. **Remove** | |
| 180 | 180 | (`admin_remove_alias`) needs a reason. Both go in sudo's audit log | |
| 181 | 181 | (`alias_added`, `alias_removed`), naming the staff member. `@g1t` in | |
| 182 | − | text still means g1t's agent: it links to how the agent works, never to | |
| 183 | − | `/g1t`. | |
| 182 | + | text still means g1t itself (Ask g1t), never the alias's `/g1t`. | |
| 184 | 183 | - **Enterprises**: customers that pay for several workspaces with one | |
| 185 | 184 | bill, one limit and one set of terms. Each has its workspaces (add or | |
| 186 | 185 | remove them), combined usage, terms, credits, ledger and audit log, and | |
| ⋯ | |||
| 295 | 294 | ||
| 296 | 295 | ```sh | |
| 297 | 296 | npm run typecheck -w @g1t/sudo | |
| 298 | − | npm test -w @g1t/sudo # JWT verification, forms, money, the workspace join, paging, nav, charts, signals, models | |
| 297 | + | npm test -w @g1t/sudo # JWT verification, forms, money, the workspace join, paging, nav, charts, signals | |
| 299 | 298 | npm run build -w @g1t/sudo | |
| 300 | 299 | ``` | |
| 301 | 300 | ||
| 59 | 59 | assert.equal(accountPath("ws_acme"), "/workspaces/acme"); | |
| 60 | 60 | assert.equal(accountPath("ent_bigco"), "/enterprises/ent_bigco"); | |
| 61 | 61 | assert.equal(accountPath("stripe"), null); | |
| 62 | − | // Shared invite links are filed under `ws_invites`: a reserved name, never a workspace. | |
| 63 | − | assert.equal(accountPath("ws_invites"), "/invites?tab=shared"); | |
| 62 | + | // Old shared invite links were filed under `ws_invites`: a reserved name, never a workspace. | |
| 63 | + | assert.equal(accountPath("ws_invites"), "/invites"); | |
| 64 | 64 | assert.equal(accountName("ws_invites"), "Shared invite links"); | |
| 65 | 65 | assert.equal(accountPath("acme"), null); | |
| 66 | 66 | assert.equal(accountPath("ws_-bad"), null); |
| 67 | 67 | */ | |
| 68 | 68 | export function accountPath(account: string | null | undefined): string | null { | |
| 69 | 69 | const id = (account ?? "").trim().toLowerCase(); | |
| 70 | − | // Shared invite links (identity's shared_invites.rs): `invites` is a reserved name, never a workspace's. | |
| 71 | − | if (id === SHARED_INVITES_ACCOUNT) return "/invites?tab=shared"; | |
| 70 | + | // Shared invite links, retired with invite-only registration: `invites` is a reserved name, never a workspace's. | |
| 71 | + | if (id === SHARED_INVITES_ACCOUNT) return "/invites"; | |
| 72 | 72 | const status = /^(incident|maintenance):([a-z0-9-]{1,64})$/.exec(id); | |
| 73 | 73 | if (status) return status[1] === "incident" ? `/incidents/${status[2]}` : `/incidents/maintenance/${status[2]}`; | |
| 74 | 74 | if (ENTERPRISE.test(id)) return `/enterprises/${encodeURIComponent(id)}`; |
| 190 | 190 | label: "Usage explorer", | |
| 191 | 191 | to: "/usage", | |
| 192 | 192 | icon: "usage", | |
| 193 | − | about: "What customers used, sliced by meter, model, repository and day.", | |
| 193 | + | about: "What customers used, sliced by meter, repository and day.", | |
| 194 | 194 | soon: { | |
| 195 | 195 | summary: [ | |
| 196 | 196 | "What customers used, across every workspace, sliced any way: by meter, repository and day. It answers the questions that come up in a sales call or a cost review: who uses the most sandbox time, and what a customer spends per pull request.", | |
| ⋯ | |||
| 198 | 198 | ], | |
| 199 | 199 | plans: [ | |
| 200 | 200 | "Charged, cost and margin over any range, for everyone or one customer", | |
| 201 | − | "Group by meter, model, task, repository or workspace; compare two periods", | |
| 201 | + | "Group by meter, repository or workspace; compare two periods", | |
| 202 | 202 | "The heaviest workspaces this month, and how fast each is growing", | |
| 203 | 203 | "Export to CSV", | |
| 204 | 204 | ], | |
| 84 | 84 | /** | |
| 85 | 85 | * What g1t absorbs of real cost on a goodwill credit: whatever is past its | |
| 86 | 86 | * margin on the overage. The margin is money g1t never had; the rest it | |
| 87 | − | * paid Cloudflare and model providers for. | |
| 87 | + | * paid Cloudflare for. | |
| 88 | 88 | */ | |
| 89 | 89 | export function absorbedMicros(amountMicros: number, marginMicros: number): number { | |
| 90 | 90 | return Math.max(0, amountMicros - Math.max(0, marginMicros)); |
| 143 | 143 | </h2> | |
| 144 | 144 | <p className="mt-2 text-sm text-fg-soft"> | |
| 145 | 145 | A {usd(review.amountMicros)} credit to <span className="font-mono">{review.workspace}</span> is past the {usd(cap)} a goodwill credit | |
| 146 | − | absorbs in one click. That is money g1t already paid Cloudflare and model providers. Say why, for whoever looks next; it goes in the | |
| 146 | + | absorbs in one click. That is money g1t already paid Cloudflare. Say why, for whoever looks next; it goes in the | |
| 147 | 147 | audit log with your email. | |
| 148 | 148 | </p> | |
| 149 | 149 | <form method="post" action={`${pathname}#review`} className="mt-4 space-y-3"> |
| 95 | 95 | {(prices || pricesError) && ( | |
| 96 | 96 | <Section | |
| 97 | 97 | title="The price book today" | |
| 98 | − | description={ | |
| 99 | − | prices ? `Read-only. Models are charged at cost plus ${prices.modelMarginPercent}%.` : undefined | |
| 100 | − | } | |
| 98 | + | description={prices ? "Read-only." : undefined} | |
| 101 | 99 | className="mt-8" | |
| 102 | 100 | > | |
| 103 | 101 | {pricesError ? ( |
| 1 | − | This policy explains what information g1t collects, why, where it goes, how long we keep it, and what you can do about it. It covers g1t.sh, api.g1t.sh, mcp.g1t.sh, docs.g1t.sh, g1t.page and the agents and sandboxes g1t runs. g1t is run by Flagon, Inc. ("Flagon", "we"), which is responsible for your information. | |
| 1 | + | This policy explains what information g1t collects, why, where it goes, how long we keep it, and what you can do about it. It covers g1t.sh, api.g1t.sh, mcp.g1t.sh, docs.g1t.sh, g1t.page and the sandboxes g1t runs. g1t is run by Flagon, Inc. ("Flagon", "we"), which is responsible for your information. | |
| 2 | 2 | ||
| 3 | 3 | The short version: we collect what we need to run a git platform for you and your agents, we don't sell it, we don't advertise, we don't use your private content to train AI models, and the only analytics on g1t.sh counts how pages are used without reading what is in your workspaces. | |
| 4 | 4 | ||
| ⋯ | |||
| 25 | 25 | ||
| 26 | 26 | ### What agents do | |
| 27 | 27 | ||
| 28 | − | When an agent runs, we keep a **session**: what it was asked, the steps it took, the tools it called, what it wrote, which model it used, what it cost, and how the run ended. Run credentials and model keys are removed from what is recorded. A session is as visible as its project: **on a public project, anyone can read it**. Workflow and check logs are kept the same way. | |
| 28 | + | When a coding agent you connect records its work on g1t, we keep a **session**: what it was asked, the steps and tools it reports, what it wrote, which model it says it used, and how it ended. Credentials are removed from what is recorded. When you ask g1t a question, the conversation is kept with your direct message with @g1t. A session is as visible as its project: **on a public project, anyone can read it**. Workflow and check logs are kept the same way. | |
| 29 | 29 | ||
| 30 | 30 | To catch cryptocurrency mining, every sandbox samples its own CPU use, disk and network activity, and the command lines of the processes it runs. These measurements are used only for abuse detection. | |
| 31 | 31 | ||
| ⋯ | |||
| 84 | 84 | ||
| 85 | 85 | We use your information to: | |
| 86 | 86 | ||
| 87 | − | - run g1t: sign you in, store and show your content to the people allowed to see it, run the agents, checks and deployments you ask for; | |
| 87 | + | - run g1t: sign you in, store and show your content to the people allowed to see it, run the checks, workflows and deployments you ask for; | |
| 88 | 88 | - keep g1t secure and fair: detect abuse, mining and fraud, enforce limits, and investigate problems; | |
| 89 | 89 | - bill workspaces and keep the records the law requires; | |
| 90 | 90 | - send you the email g1t needs to: confirming your address, resetting your password, billing alerts and receipts, answers to requests you made, and important changes to g1t or these policies. **We don't send marketing email**; | |
| ⋯ | |||
| 100 | 100 | We share information only to run g1t, at your direction, or when the law requires it. | |
| 101 | 101 | ||
| 102 | 102 | - **Service providers ("subprocessors")** that run parts of g1t for us, under contracts that limit them to doing so: Cloudflare (hosting, storage, databases, sandboxes, email and search), Stripe (payments) and HeyCatch (site analytics). The [subprocessors](/policies/subprocessors) page lists them and what each receives. | |
| 103 | − | - **Services you connect.** When you connect an issue tracker, error tracking or a webhook, or let your sandboxes reach a host, we send them what that connection needs, because you asked us to. | |
| 103 | + | - **Services you connect.** When you connect an AI provider, an issue tracker, error tracking or a webhook, or let your sandboxes reach a host, we send them what that connection needs, because you asked us to. | |
| 104 | 104 | - **Other people on g1t**, as your settings allow: your workspace's members, and everyone for public projects and your public profile. | |
| 105 | 105 | - **Vulnerability databases.** To find vulnerable dependencies, we send the names and versions of packages in your lockfiles to OSV.dev, an open database run by Google. Nothing else about you or your repository is sent. | |
| 106 | 106 | - **The law.** We disclose information when we must by law, such as a valid court order, and when we need to protect the rights, property or safety of our users, the public or Flagon. Where we can, we tell you first. | |
| 2 | 2 | ||
| 3 | 3 | ## What is charged, and why | |
| 4 | 4 | ||
| 5 | − | The forge is free. What costs money is compute (agents, sandboxes, checks, workflows, the merge queue, deployments, and storage and git traffic past what is included) at what it costs us plus 20%, and the g1t plan, $20 a month per workspace with $10 of usage included. Because usage is charged at close to what it costs us, most of it can't be given back once it has run: we've already paid for it. | |
| 5 | + | The forge is free. What costs money is compute (sandboxes, checks, workflows, the merge queue, deployments, and storage and git traffic past what is included) at what it costs us plus 20%, and the g1t plan, $20 a month per workspace with $10 of usage included. Because usage is charged at close to what it costs us, most of it can't be given back once it has run: we've already paid for it. | |
| 6 | 6 | ||
| 7 | 7 | That's why g1t works hard to keep spending from surprising you: a $5 trial that never turns into a charge, a spend limit you set, caps per run and per issue, alerts at 50%, 75%, 90% and 100%, and a pause when spending suddenly spikes until an owner says to keep going. | |
| 8 | 8 | ||
| 9 | 9 | ## Spent more than you meant to | |
| 10 | 10 | ||
| 11 | − | If a month went well past what you meant to spend (a runaway job, an agent that kept going, a loop you didn't notice), tell us. On the workspace's **Billing** page, an owner chooses **Spent more than you meant to? Tell us.**, says what happened, and sends it. | |
| 11 | + | If a month went well past what you meant to spend (a runaway job, a workflow that kept going, a loop you didn't notice), tell us. On the workspace's **Billing** page, an owner chooses **Spent more than you meant to? Tell us.**, says what happened, and sends it. | |
| 12 | 12 | ||
| 13 | 13 | - **Once every 12 months per workspace**, g1t can credit the overage above your usual month. | |
| 14 | 14 | - **The 20% we added to that overage always comes back to you.** We don't keep a margin on a mistake. |
| 4 | 4 | ||
| 5 | 5 | | Company | What they do for g1t | What they receive | Where | | |
| 6 | 6 | | --- | --- | --- | --- | | |
| 7 | − | | **Cloudflare, Inc.** | Hosting and the network g1t runs on (Workers), databases (D1), storage for repositories, avatars and screenshots (Artifacts, KV and R2), queues, the sandboxes agents and checks run in (Containers), sending email (Email Service), search embeddings (Workers AI and Vectorize), and screenshots of deployed apps (Browser Rendering) | Everything stored on g1t, and every request to it | Global | | |
| 7 | + | | **Cloudflare, Inc.** | Hosting and the network g1t runs on (Workers), databases (D1), storage for repositories, avatars and screenshots (Artifacts, KV and R2), queues, the sandboxes checks, workflows and builds run in (Containers), sending email (Email Service), search embeddings (Workers AI and Vectorize), and screenshots of deployed apps (Browser Rendering) | Everything stored on g1t, and every request to it | Global | | |
| 8 | 8 | | **Stripe, Inc.** | Payments, cards, invoices and receipts | Workspace owners' billing details, card details (entered on Stripe's page, never on g1t), and what each invoice charges | United States | | |
| 9 | 9 | | **HeyCatch, Inc.** | Product analytics on g1t.sh: page views and clicks. Visitors in the EU, EEA, UK and Switzerland are asked first | The page's address (names replaced outside the public pages), the text of what is clicked on the public pages, browser and device type, IP address, the referring site, an analytics id, and a signed-in person's internal account id. Never repository content, chat, page titles inside the app, what you type, or recordings of your session | United States | | |
| 10 | 10 | ||
| ⋯ | |||
| 18 | 18 | ||
| 19 | 19 | When you connect these, your information goes to them because you asked, under your own agreement with them. They are not g1t's subprocessors: | |
| 20 | 20 | ||
| 21 | − | - **Integrations**, such as GitHub, Linear, Jira, Sentry and Datadog. | |
| 21 | + | - **Integrations**, such as your workspace's AI provider, GitHub, Linear, Jira, Sentry and Datadog. | |
| 22 | 22 | - **Webhooks** you add, which send events to the addresses you choose. | |
| 23 | 23 | - **Hosts your sandboxes reach**, such as package registries and anything your jobs and builds fetch from or send to. | |
| 24 | 24 | - **Custom domains** you point at your deployments. | |
| 1 | − | These terms are the agreement between you and Flagon, Inc. ("Flagon", "we", "us") when you use g1t: the website at g1t.sh, its API at api.g1t.sh, its MCP server at mcp.g1t.sh, git over g1t.sh, the documentation, apps deployed to g1t.page, and the agents and sandboxes g1t runs for you (together, "g1t" or "the service"). | |
| 1 | + | These terms are the agreement between you and Flagon, Inc. ("Flagon", "we", "us") when you use g1t: the website at g1t.sh, its API at api.g1t.sh, its MCP server at mcp.g1t.sh, git over g1t.sh, the documentation, apps deployed to g1t.page, and the sandboxes g1t runs for you (together, "g1t" or "the service"). | |
| 2 | 2 | ||
| 3 | 3 | We've tried to write them the way we'd want to read them: plainly. If anything here is unclear, write to [hey@flagon.io](mailto:hey@flagon.io) and we'll explain it. | |
| 4 | 4 | ||
| ⋯ | |||
| 10 | 10 | ||
| 11 | 11 | - **You need an account to do most things.** Give us a real email address and keep it current: it's how we reach you about your account, security and billing, and how you get back in if you lose your password. | |
| 12 | 12 | - **You must be at least 13 years old**, and old enough where you live to agree to these terms yourself. If you're under 18, a parent or guardian must agree to them for you. | |
| 13 | − | - **An account is for one person.** Automation is welcome: use access tokens, workspace tokens and agents for it. Don't share a password, and don't create accounts to get around a limit, a trial, or a suspension. | |
| 13 | + | - **An account is for one person.** Automation is welcome: use access tokens, workspace tokens and coding agents for it. Don't share a password, and don't create accounts to get around a limit, a trial, or a suspension. | |
| 14 | 14 | - **Keep your credentials safe.** You're responsible for what happens under your account, your tokens and your SSH keys. If you think someone else has them, revoke them in your settings and tell us at [hey@flagon.io](mailto:hey@flagon.io). | |
| 15 | 15 | ||
| 16 | 16 | ## 2. Workspaces | |
| 17 | 17 | ||
| 18 | − | Everything on g1t lives in a workspace: its projects, repositories, issues, pull requests, agents, settings and bill. A workspace's **owners** control it. They decide who is a member, what members and agents may do, and they're responsible for paying for it. If you create a workspace for an organization, the organization is the customer, and its owners act for it. | |
| 18 | + | Everything on g1t lives in a workspace: its projects, repositories, issues, pull requests, settings and bill. A workspace's **owners** control it. They decide who is a member, what members and agents may do, and they're responsible for paying for it. If you create a workspace for an organization, the organization is the customer, and its owners act for it. | |
| 19 | 19 | ||
| 20 | 20 | ## 3. Acceptable use | |
| 21 | 21 | ||
| ⋯ | |||
| 28 | 28 | To run g1t, we need your permission to handle your content. You give Flagon a worldwide, non-exclusive, royalty-free license to host, store, copy, process, transmit and display your content **only as needed to provide, secure and improve the service for you**. That includes, for example: | |
| 29 | 29 | ||
| 30 | 30 | - storing your repositories and showing them to the people you let see them; | |
| 31 | − | - sending the relevant parts to a model provider when you ask an agent to work (see section 6); | |
| 31 | + | - sending the relevant parts to your workspace's AI provider when you ask g1t a question (see section 6); | |
| 32 | 32 | - indexing it so you can search it; | |
| 33 | 33 | - scanning it for secrets and vulnerable dependencies, so we can warn you; | |
| 34 | 34 | - making backups. | |
| 35 | 35 | ||
| 36 | 36 | This license ends when your content is deleted from g1t, except for copies in backups (which expire on their own schedule) and content others have a right to keep under section 5. | |
| 37 | 37 | ||
| 38 | − | **We don't sell your content, and we don't use your private content to train AI models.** We don't let the model providers we use for hosted agents train on it either. | |
| 38 | + | **We don't sell your content, and we don't use your private content to train AI models.** g1t itself doesn't send your content to any model provider: Ask g1t uses the AI provider your workspace connects, under your agreement with them. | |
| 39 | 39 | ||
| 40 | 40 | You're responsible for your content: that you have the right to put it on g1t, and that it doesn't break the law or someone else's rights. | |
| 41 | 41 | ||
| ⋯ | |||
| 49 | 49 | ||
| 50 | 50 | ## 6. Agents act on your behalf | |
| 51 | 51 | ||
| 52 | − | g1t's agents ("g1t agents") write code, review changes, answer questions, plan work and land changes, when you or another member of your workspace asks them to: by assigning an issue, mentioning an agent, starting a plan, or turning on something that runs on its own, such as upkeep or automatic merges. | |
| 52 | + | The coding agents you connect to g1t (through the MCP server, the API, the g1t CLI or a plugin) work with your access: they read and change what you let them, and open issues and pull requests, record their sessions and claim work as you. | |
| 53 | 53 | ||
| 54 | − | - **An agent acts on behalf of the person who started its work**, within the limits of that person's access and the workspace's guardrails. Its actions are recorded as such in the audit log. | |
| 54 | + | - **An agent acts on behalf of the person whose credentials it uses**, within the limits of that person's access, the token's scopes and the workspace's rules. Its actions are recorded as such in the audit log. | |
| 55 | 55 | - **You're responsible for what you ask agents to do**, and for checking what they produce before you rely on it, merge it or deploy it, as you would for a teammate's work. Agents make mistakes. Their output can be wrong, insecure, or similar to code that exists elsewhere, and you're responsible for making sure your use of it complies with the licenses and laws that apply. | |
| 56 | − | - **Guardrails** (time caps on runs) reduce risk. They're a safety net, not a guarantee that an agent won't do something you didn't intend. | |
| 57 | 56 | - **What agents produce for you is your content.** As between you and us, Flagon claims no rights in it. | |
| 58 | − | - **Agents cost money.** Runs, sandbox time and model use are charged to the workspace that owns the repository, as described on [pricing](/pricing). | |
| 59 | 57 | ||
| 60 | − | To run hosted agents, we send the relevant content (such as the issue, the code it needs and the conversation so far) to a model provider. The [subprocessors](/policies/subprocessors) page lists who. If your workspace connects its own model provider, your content goes to that provider instead, under your agreement with them. | |
| 58 | + | **Ask g1t** answers questions about your workspace with the AI provider your workspace connects. To answer, we send that provider the question and the content it needs (such as the issue, pull request or files it reads), under your agreement with them. Its answers can be wrong too. | |
| 61 | 59 | ||
| 62 | 60 | ## 7. Services you connect | |
| 63 | 61 | ||
| ⋯ | |||
| 66 | 64 | ## 8. Paying for g1t | |
| 67 | 65 | ||
| 68 | 66 | - **The forge is free.** Repositories, git, issues, pull requests and review cost nothing, within the free limits described on [pricing](/pricing). | |
| 69 | − | - **Compute is paid.** Agents, sandboxes, checks, workflows, the merge queue, deployments and the other metered items on [pricing](/pricing) are charged to the workspace, at what they cost us plus the markup shown there. **The g1t plan** is a monthly subscription per workspace, with some usage included. | |
| 67 | + | - **Compute is paid.** Sandboxes, checks, workflows, the merge queue, deployments and the other metered items on [pricing](/pricing) are charged to the workspace, at what they cost us plus the markup shown there. **The g1t plan** is a monthly subscription per workspace, with some usage included. | |
| 70 | 68 | - **Usage prices follow our costs.** They move when what we pay our providers moves, and every change is listed on the pricing page with its reason. We'll give owners at least 30 days' notice by email before we raise the plan's own monthly price. | |
| 71 | 69 | - **Payment.** Payments are processed by Stripe. You authorize us to charge the workspace's card, or another payment method on file, for the plan, for usage as it is billed, and when the workspace nears its limit. Invoices are available from the Billing page. Card numbers never reach g1t. | |
| 72 | 70 | - **Taxes.** Prices don't include taxes unless we say so. You're responsible for the taxes that apply to your purchases, other than taxes on our income. | |
| ⋯ | |||
| 106 | 104 | ||
| 107 | 105 | ## 12. Disclaimers | |
| 108 | 106 | ||
| 109 | − | We work hard to make g1t reliable, but **g1t is provided "as is" and "as available."** To the extent the law allows, Flagon disclaims all warranties, express or implied, including merchantability, fitness for a particular purpose, title and non-infringement. We don't promise that g1t will be uninterrupted or error-free, that agents' output will be correct or fit for your purpose, or that content will never be lost. Keep your own copies of what matters to you: git makes this easy. | |
| 107 | + | We work hard to make g1t reliable, but **g1t is provided "as is" and "as available."** To the extent the law allows, Flagon disclaims all warranties, express or implied, including merchantability, fitness for a particular purpose, title and non-infringement. We don't promise that g1t will be uninterrupted or error-free, that answers or agents' output will be correct or fit for your purpose, or that content will never be lost. Keep your own copies of what matters to you: git makes this easy. | |
| 110 | 108 | ||
| 111 | 109 | ## 13. Limitation of liability | |
| 112 | 110 | ||
| 43 | 43 | }); | |
| 44 | 44 | ||
| 45 | 45 | test("search finds by name, description, tag and keyword, across every category", () => { | |
| 46 | − | const found = arrange(workspace, { category: "design", query: "sentry", connected: none }); | |
| 46 | + | const found = arrange(workspace, { category: "design", query: "resolve", connected: none }); | |
| 47 | 47 | assert.deepEqual(found.available.map((view) => view.id), ["sentry"], "a search is not held to the category"); | |
| 48 | − | assert.deepEqual(found.soon.map((view) => view.id), ["sentry-mcp"], "what is coming sorts after what works"); | |
| 48 | + | assert.deepEqual(found.soon.map((view) => view.id), ["pagerduty"], "what is coming sorts after what works"); | |
| 49 | 49 | assert.ok(matchesQuery(personal.find((view) => view.id === "google-calendar")!, "in a meeting")); | |
| 50 | 50 | assert.ok(matchesQuery(workspace.find((view) => view.id === "anthropic")!, "claude")); | |
| 51 | 51 | assert.ok(matchesQuery(workspace.find((view) => view.id === "incident-io")!, "incidentio")); | |
| 52 | 52 | assert.ok(matchesQuery(workspace.find((view) => view.id === "pagerduty")!, "opens ISSUES")); | |
| 53 | − | assert.ok(!matchesQuery(workspace.find((view) => view.id === "figma")!, "kubernetes")); | |
| 53 | + | assert.ok(!matchesQuery(workspace.find((view) => view.id === "miro")!, "kubernetes")); | |
| 54 | 54 | }); | |
| 55 | 55 | ||
| 56 | 56 | test("the navigation counts each category and leaves out empty ones", () => { |
| 97 | 97 | export const POLICIES_DRAFT = true; | |
| 98 | 98 | ||
| 99 | 99 | /** When any policy last changed, `YYYY-MM-DD`. */ | |
| 100 | − | export const POLICIES_UPDATED = "2026-10-09"; | |
| 100 | + | export const POLICIES_UPDATED = "2026-10-13"; | |
| 101 | 101 | ||
| 102 | 102 | /** Every change to the policies, newest first. */ | |
| 103 | 103 | export const POLICY_HISTORY: { date: string; change: string }[] = [ | |
| 104 | 104 | { | |
| 105 | + | date: "2026-10-13", | |
| 106 | + | change: | |
| 107 | + | "Terms of Service and Privacy Policy: g1t no longer runs agents of its own. Section 6 of the terms covers the coding agents you connect and Ask g1t, which answers with your workspace's own AI provider; agents are no longer billed compute. Subprocessors: your workspace's AI provider listed among the integrations you choose.", | |
| 108 | + | }, | |
| 109 | + | { | |
| 105 | 110 | date: "2026-10-09", | |
| 106 | 111 | change: | |
| 107 | 112 | "Privacy Policy: g1t.sh uses HeyCatch for site analytics, with names removed outside the public pages, and one analytics cookie, set in the EU, EEA, UK and Switzerland only after you allow it. Subprocessors: HeyCatch, Inc. added.", |
| 108 | 108 | */ | |
| 109 | 109 | const READS = new Set( | |
| 110 | 110 | ( | |
| 111 | − | "account active_agents all_ids get list queue pulls_for_repos blame blob branches by_author by_repo contributions catalog check_invite check_limit " + | |
| 112 | − | "check_workspace_deletion check_workspace_rename collaborator_permission compare counts deleted deliveries " + | |
| 113 | − | "domains entitlements entity explore features git_access has_feature invoices ledger limit " + | |
| 114 | − | "limit_requests links log logs managed_pulls my_repo_invitations " + | |
| 115 | − | "outside_collaborators overview path_by_id prices profile profile_workspaces public_namespaces read_session " + | |
| 116 | − | "readable ready_issues references registration repo_access resolve resolve_branch resolve_path resolve_slug " + | |
| 117 | − | "routes run run_context run_cost runner_activity runner_groups runner_settings runners runs scorecards search usage_report " + | |
| 118 | − | "settings statement statement_entries status status_by_id suggest tree usage usage_meters user_by_username " + | |
| 119 | − | "user_for_session user_for_access_token usernames waiting_workspaces workflows workspace workspace_invites github_enabled " + | |
| 120 | − | "stars about public_links branch_drift tags last_commits languages contributors license releases release " + | |
| 121 | − | "stargazers starred commit_checks shortcuts spend person_budgets usage_report templates install_requests " + | |
| 122 | − | "marketplaces discover plugin plugin_installs plugin_agents connectors connector agent_connectors customize_bundle " + | |
| 123 | − | "people_directory team_agents team_context team_members github_visible_installations skill_library skill agent_skills last_visit team_agent_index " + | |
| 111 | + | "account all_ids get list queue pulls_for_repos blame blob branches by_author by_repo contributions " + | |
| 112 | + | "check_invite check_limit check_workspace_deletion check_workspace_rename collaborator_permission " + | |
| 113 | + | "compare counts deleted deliveries domains entitlements explore features git_access has_feature " + | |
| 114 | + | "invoices ledger limit limit_requests links log logs managed_pulls my_repo_invitations " + | |
| 115 | + | "outside_collaborators overview path_by_id prices profile profile_workspaces public_namespaces " + | |
| 116 | + | "read_session readable repo_access resolve resolve_branch resolve_path resolve_slug " + | |
| 117 | + | "routes run runner_activity runner_groups runner_settings runners runs search usage_report settings " + | |
| 118 | + | "statement statement_entries status status_by_id suggest tree usage usage_meters user_by_username " + | |
| 119 | + | "user_for_session user_for_access_token usernames workflows workspace workspace_invites " + | |
| 120 | + | "github_enabled stars about public_links branch_drift tags last_commits languages contributors " + | |
| 121 | + | "license releases release stargazers starred commit_checks shortcuts spend usage_report templates " + | |
| 122 | + | "people_directory team_members github_visible_installations last_visit " + | |
| 124 | 123 | "activity_digest activity" | |
| 125 | 124 | ).split(" "), | |
| 126 | 125 | ); |
| 1 | − | .cache/ |
| 1 | − | # syntax=docker/dockerfile:1.7 | |
| 2 | − | # | |
| 3 | − | # g1t's reviewer as a ReviewBench agent image: the g1t sandbox's base image | |
| 4 | − | # (the Claude Code CLI and toolchains production reviews run with) plus the | |
| 5 | − | # contract adapter in agent/. Built by `node bench/reviewbench/run.mjs build`, | |
| 6 | − | # which passes BASE from services/runner/base.json and regenerates | |
| 7 | − | # agent/instructions.txt from crates/runner/src/review.rs first. | |
| 8 | − | # | |
| 9 | − | # linux/amd64, as the contract requires. | |
| 10 | − | ||
| 11 | − | ARG BASE | |
| 12 | − | FROM --platform=linux/amd64 ${BASE} | |
| 13 | − | COPY agent/ /bench/ | |
| 14 | − | # /work/change.diff and /work/review.json are where the production prompt | |
| 15 | − | # tells the agent to read and write; the benchmark mounts the rest of /work. | |
| 16 | − | RUN mkdir -p /work && chmod 0777 /work | |
| 17 | − | ENTRYPOINT ["node", "/bench/agent.mjs"] |
| 1 | − | // g1t's pull request reviewer behind the ReviewBench agent contract | |
| 2 | − | // (https://github.com/review-bench/ReviewBench/blob/main/AGENT_CONTRACT.md). | |
| 3 | − | // | |
| 4 | − | // Runs inside the image built from bench/reviewbench/Dockerfile, which is | |
| 5 | − | // the g1t sandbox's base image (same Claude Code CLI, same toolchains). | |
| 6 | − | // It does what crates/runner/src/review.rs does after its clone: write the | |
| 7 | − | // change to /work/change.diff, run Claude Code headless with the same | |
| 8 | − | // flags as crates/runner/src/harness.rs, read /work/review.json, and apply | |
| 9 | − | // the same filters services/work/src/reviews.rs applies when it records a | |
| 10 | − | // review. Then it writes the review's line comments as findings. | |
| 11 | − | // | |
| 12 | − | // Configuration (all optional): | |
| 13 | − | // ANTHROPIC_MODEL or RB_CONFIG_MODEL model id (production review route: claude-sonnet-5-5) | |
| 14 | − | // ANTHROPIC_BASE_URL g1t's model proxy (MODELS_URL/anthropic) or a gateway | |
| 15 | − | // ANTHROPIC_API_KEY provider key, or a model-proxy session token | |
| 16 | − | // RB_CONFIG_BUDGET_USD per-PR cost cap (--max-budget-usd), default 5 | |
| 17 | − | // RB_CONFIG_VARIANT prompt variant: "production" (default) or a file in variants/ | |
| 18 | − | ||
| 19 | − | import { spawn } from "node:child_process"; | |
| 20 | − | import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs"; | |
| 21 | − | ||
| 22 | − | import { buildPrompt } from "./prompt-lib.mjs"; | |
| 23 | − | ||
| 24 | − | const REPO = "/work/repo"; | |
| 25 | − | const DIFF_FILE = "/work/change.diff"; | |
| 26 | − | const REVIEW_FILE = "/work/review.json"; | |
| 27 | − | const MAX_TURNS = "80"; // harness.rs | |
| 28 | − | const MAX_REVIEW_COMMENTS = 30; // reviews.rs | |
| 29 | − | const MAX_REVIEW_CHARS = 20_000; // reviews.rs | |
| 30 | − | ||
| 31 | − | const env = (name, fallback) => process.env[name] ?? fallback; | |
| 32 | − | const out = env("RB_OUT", "/work/out/findings.json"); | |
| 33 | − | const agent = env("RB_AGENT", "g1t"); | |
| 34 | − | const pr = JSON.parse(readFileSync(env("RB_PR_JSON", "/work/pr/pr.json"), "utf8")); | |
| 35 | − | const model = env("RB_CONFIG_MODEL", env("ANTHROPIC_MODEL", "claude-sonnet-5-5")); | |
| 36 | − | const budget = Number(env("RB_CONFIG_BUDGET_USD", "5")); | |
| 37 | − | const variant = env("RB_CONFIG_VARIANT", "production"); | |
| 38 | − | ||
| 39 | − | // The run's settings, printed so ReviewBench can see each label took effect. | |
| 40 | − | console.log(`model=${model} budget_usd=${budget} variant=${variant}`); | |
| 41 | − | ||
| 42 | − | function write(findings, usage) { | |
| 43 | − | const body = { | |
| 44 | − | pr: { repo: pr.repo ?? `https://github.com/${env("RB_NWO")}`, pr_number: Number(env("RB_PR_NUMBER", pr.pr_number)), base: env("RB_BASE", pr.base), head: env("RB_HEAD", pr.head) }, | |
| 45 | − | agent, | |
| 46 | − | findings, | |
| 47 | − | usage, | |
| 48 | − | }; | |
| 49 | − | writeFileSync(out, JSON.stringify(body, null, 2)); | |
| 50 | − | } | |
| 51 | − | ||
| 52 | − | /** Paths the change touches, as the work service knows them (pull.files). */ | |
| 53 | − | function changedPaths(diff) { | |
| 54 | − | const paths = new Set(); | |
| 55 | − | for (const line of diff.split("\n")) { | |
| 56 | − | const m = line.match(/^\+\+\+ b\/(.+)$/) ?? line.match(/^--- a\/(.+)$/); | |
| 57 | − | if (m && m[1] !== "/dev/null") paths.add(m[1]); | |
| 58 | − | } | |
| 59 | − | return paths; | |
| 60 | − | } | |
| 61 | − | ||
| 62 | − | function runClaude(prompt) { | |
| 63 | − | return new Promise((resolve, reject) => { | |
| 64 | − | const args = ["--print", prompt, "--output-format", "stream-json", "--verbose", "--max-turns", MAX_TURNS, "--dangerously-skip-permissions", "--model", model]; | |
| 65 | − | if (budget > 0) args.push("--max-budget-usd", budget.toFixed(2)); | |
| 66 | − | const child = spawn("claude", args, { cwd: REPO, stdio: ["ignore", "pipe", "inherit"], env: { ...process.env, ANTHROPIC_MODEL: model } }); | |
| 67 | − | let result = null; | |
| 68 | − | let buffer = ""; | |
| 69 | − | child.stdout.on("data", (chunk) => { | |
| 70 | − | buffer += chunk; | |
| 71 | − | let at; | |
| 72 | − | while ((at = buffer.indexOf("\n")) >= 0) { | |
| 73 | − | const line = buffer.slice(0, at); | |
| 74 | − | buffer = buffer.slice(at + 1); | |
| 75 | − | try { | |
| 76 | − | const event = JSON.parse(line); | |
| 77 | − | if (event.type === "result") result = event; | |
| 78 | − | } catch {} | |
| 79 | − | } | |
| 80 | − | }); | |
| 81 | − | child.on("error", reject); | |
| 82 | − | child.on("close", (code) => (result ? resolve(result) : reject(new Error(`claude exited ${code} without a result`)))); | |
| 83 | − | }); | |
| 84 | − | } | |
| 85 | − | ||
| 86 | − | const started = Date.now(); | |
| 87 | − | copyFileSync(env("RB_DIFF", "/work/pr/diff.patch"), DIFF_FILE); | |
| 88 | − | const diff = readFileSync(DIFF_FILE, "utf8"); | |
| 89 | − | if (!diff.trim()) { | |
| 90 | − | // review.rs bails on an empty change; the benchmark still wants a file. | |
| 91 | − | write([], { time_in_ms: Date.now() - started }); | |
| 92 | − | process.exit(0); | |
| 93 | − | } | |
| 94 | − | ||
| 95 | − | const instructionsFile = variant === "production" ? new URL("./instructions.txt", import.meta.url) : new URL(`./variants/${variant}.txt`, import.meta.url); | |
| 96 | − | const prompt = buildPrompt(pr, readFileSync(instructionsFile, "utf8")); | |
| 97 | − | const result = await runClaude(prompt); | |
| 98 | − | ||
| 99 | − | let review = null; | |
| 100 | − | if (existsSync(REVIEW_FILE)) { | |
| 101 | − | try { | |
| 102 | − | review = JSON.parse(readFileSync(REVIEW_FILE, "utf8")); | |
| 103 | − | } catch {} | |
| 104 | − | } | |
| 105 | − | // review.rs: no file means the agent's answer is the review, with no line comments. | |
| 106 | − | const comments = Array.isArray(review?.comments) ? review.comments : []; | |
| 107 | − | const known = changedPaths(diff); | |
| 108 | − | const findings = comments | |
| 109 | − | .filter((c) => typeof c?.body === "string" && c.body.trim() && typeof c.path === "string") | |
| 110 | − | // reviews.rs: a line in a file the pull request does not change is dropped. | |
| 111 | − | .filter((c) => known.size === 0 || known.has(c.path.replace(/^\.\//, ""))) | |
| 112 | − | .slice(0, MAX_REVIEW_COMMENTS) | |
| 113 | − | .map((c) => { | |
| 114 | − | const line = Number.isInteger(c.line) && c.line > 0 ? c.line : 1; | |
| 115 | − | const end = Number.isInteger(c.end_line) && c.end_line >= line ? c.end_line : line; | |
| 116 | − | return { file: c.path.replace(/^\.\//, ""), start_line: line, end_line: end, message: c.body.trim().slice(0, MAX_REVIEW_CHARS), producer: agent }; | |
| 117 | − | }); | |
| 118 | − | ||
| 119 | − | // Kept beside the findings for analysis: what production would also have | |
| 120 | − | // posted (verdict, summary) and what the run cost. | |
| 121 | − | writeFileSync(out.replace(/\.json$/, ".g1t.json"), JSON.stringify({ verdict: review?.verdict ?? null, body: review?.body ?? result.result ?? "", dropped: comments.length - findings.length, cost_usd: result.total_cost_usd, turns: result.num_turns, model }, null, 2)); | |
| 122 | − | write(findings, { time_in_ms: Date.now() - started, ...(typeof result.total_cost_usd === "number" ? { cost_usd: result.total_cost_usd } : {}) }); |
| 1 | − | You are reviewing a pull request. The repository is checked out in the current directory at the pull request's head. The change under review is in /work/change.diff; read it first, then read the surrounding code as needed. You may run the project's tests. Do not modify the repository. | |
| 2 | − | ||
| 3 | − | Judge whether the change does what it is for, whether it is correct, and whether it would break anything. Be specific and brief. Comment only on real problems or things a maintainer would want to know; do not praise, and do not restate the diff. | |
| 4 | − | ||
| 5 | − | Write your review to /work/review.json as JSON with exactly this shape: | |
| 6 | − | ||
| 7 | − | { | |
| 8 | − | "verdict": "approve" or "request_changes", | |
| 9 | − | "body": "A summary in Markdown: what you checked and your conclusion.", | |
| 10 | − | "comments": [ | |
| 11 | − | { "path": "path/in/the/repository", "line": 12, "body": "What is wrong on this line and what to do about it." } | |
| 12 | − | ] | |
| 13 | − | } | |
| 14 | − | ||
| 15 | − | `line` is the line number in the file as it is after the change. `comments` may be empty. Use "request_changes" only if something must be fixed before merging. Then finish. |
| 1 | − | // The part of ../prompt.mjs the image needs, without the path to the Rust | |
| 2 | − | // source (the image only carries instructions.txt, generated from it). | |
| 3 | − | ||
| 4 | − | /** What production says about the pull request, then the instructions. */ | |
| 5 | − | export function buildPrompt(pr, instructions) { | |
| 6 | − | const about = [`Pull request #${pr.pr_number}: ${pr.title ?? ""}`, pr.body ?? ""].filter((part) => part.trim()).join("\n\n"); | |
| 7 | − | return `${about}\n\n${instructions}`; | |
| 8 | − | } |
| 1 | − | You are reviewing a pull request. The repository is checked out in the current directory at the pull request's head. The change under review is in /work/change.diff. Do not modify the repository. | |
| 2 | − | ||
| 3 | − | Work in three passes. | |
| 4 | − | ||
| 5 | − | 1. Map the change. Read the diff and list every file it touches. For each changed function, type or config, find its callers and the code it calls (grep for the names) and read them. Read the tests that cover the change, and the tests that should and do not. | |
| 6 | − | ||
| 7 | − | 2. Look for problems, file by file, in this order: correctness (wrong logic, off-by-one, null and error paths, broken invariants, concurrency, resource leaks), security (input reaching queries, shells, paths, HTML or regexes; secrets; authorization), reliability (missing error handling, retries, timeouts, migrations that cannot run twice), API and compatibility (callers this change breaks, changed behavior without a version bump), tests (missing or ineffective tests for new behavior), performance (work in loops, unbounded growth), documentation that the change makes wrong, and accessibility in UI code. Where you can, run the project's tests or a quick check to confirm a suspicion. | |
| 8 | − | ||
| 9 | − | 3. Check each candidate before keeping it. Re-read the exact lines. Drop it if it is not true of the code at head, if it is pure style, or if the change does not make it worse. Keep every distinct problem that survives; do not stop at the first few. | |
| 10 | − | ||
| 11 | − | Write your review to /work/review.json as JSON with exactly this shape: | |
| 12 | − | ||
| 13 | − | { | |
| 14 | − | "verdict": "approve" or "request_changes", | |
| 15 | − | "body": "A summary in Markdown: what you checked and your conclusion.", | |
| 16 | − | "comments": [ | |
| 17 | − | { "path": "path/in/the/repository", "line": 12, "end_line": 14, "severity": "high" | "medium" | "low", "body": "What is wrong, why it matters, and what to do about it." } | |
| 18 | − | ] | |
| 19 | − | } | |
| 20 | − | ||
| 21 | − | `line` and `end_line` are line numbers in the file as it is after the change, spanning the problem. One comment per problem. Every problem you mention in the summary must also be a comment on its lines. `comments` may be empty. Use "request_changes" only if something must be fixed before merging. Then finish. |
| 1 | − | // The review prompt, taken from the runner's own source so the benchmark | |
| 2 | − | // measures what production runs. crates/runner/src/review.rs holds the | |
| 3 | − | // instructions as a Rust string constant; services/runner/src/index.ts | |
| 4 | − | // (startReviewRun) puts "what the pull request is" in front of them. | |
| 5 | − | // | |
| 6 | − | // Production also appends repository instructions (AGENTS.md, CLAUDE.md, | |
| 7 | − | // .g1t/review.md); Claude Code reads CLAUDE.md from the checkout by | |
| 8 | − | // itself, as it does in a trusted production run. | |
| 9 | − | ||
| 10 | − | import { readFileSync } from "node:fs"; | |
| 11 | − | import { fileURLToPath } from "node:url"; | |
| 12 | − | ||
| 13 | − | const REVIEW_RS = fileURLToPath(new URL("../../crates/runner/src/review.rs", import.meta.url)); | |
| 14 | − | ||
| 15 | − | /** The `INSTRUCTIONS` constant of review.rs, unescaped as rustc would. */ | |
| 16 | − | export function instructionsFromRust(source = readFileSync(REVIEW_RS, "utf8")) { | |
| 17 | − | const match = source.match(/const INSTRUCTIONS: &str = "([\s\S]*?)";\n/); | |
| 18 | − | if (!match) throw new Error("INSTRUCTIONS not found in review.rs; update bench/reviewbench/prompt.mjs"); | |
| 19 | − | return match[1] | |
| 20 | − | // A backslash at the end of a line continues the string and eats the | |
| 21 | − | // next line's leading whitespace. | |
| 22 | − | .replace(/\\\n\s*/g, "") | |
| 23 | − | .replace(/\\"/g, '"') | |
| 24 | − | .replace(/\\n/g, "\n") | |
| 25 | − | .replace(/\\\\/g, "\\"); | |
| 26 | − | } | |
| 27 | − | ||
| 28 | − | export { buildPrompt } from "./agent/prompt-lib.mjs"; | |
| 29 | − | ||
| 30 | − | // `node prompt.mjs` writes instructions.txt for the image build, and | |
| 31 | − | // `node prompt.mjs --check` fails when it has drifted from review.rs. | |
| 32 | − | if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { | |
| 33 | − | const out = fileURLToPath(new URL("./agent/instructions.txt", import.meta.url)); | |
| 34 | − | const text = instructionsFromRust(); | |
| 35 | − | if (process.argv.includes("--check")) { | |
| 36 | − | let current = ""; | |
| 37 | − | try { | |
| 38 | − | current = readFileSync(out, "utf8"); | |
| 39 | − | } catch {} | |
| 40 | − | if (current !== text) { | |
| 41 | − | console.error("bench/reviewbench/agent/instructions.txt is out of date; run node bench/reviewbench/prompt.mjs"); | |
| 42 | − | process.exit(1); | |
| 43 | − | } | |
| 44 | − | } else { | |
| 45 | − | const { writeFileSync } = await import("node:fs"); | |
| 46 | − | writeFileSync(out, text); | |
| 47 | − | console.log(`wrote ${out} (${text.length} chars)`); | |
| 48 | − | } | |
| 49 | − | } |
| 1 | − | #!/usr/bin/env node | |
| 2 | − | // Runs g1t's pull request reviewer over ReviewBench and scores it with the | |
| 3 | − | // benchmark's own judge. | |
| 4 | − | // | |
| 5 | − | // node bench/reviewbench/run.mjs setup clone ReviewBench (pinned) and install its judge | |
| 6 | − | // node bench/reviewbench/run.mjs build build the agent image from the runner base image | |
| 7 | − | // node bench/reviewbench/run.mjs estimate --set sample:50 | |
| 8 | − | // node bench/reviewbench/run.mjs review --set sample:50 [--seed 1] [--variant production] [--model claude-sonnet-5-5] --yes | |
| 9 | − | // node bench/reviewbench/run.mjs judge --run <id> [--judge-model claude-sonnet-5] --yes | |
| 10 | − | // node bench/reviewbench/run.mjs report --run <id> [--json] | |
| 11 | − | // | |
| 12 | − | // --set is test (the benchmark's 25), full (all 219) or sample:N (N of the | |
| 13 | − | // 219, stratified by change size, reproducible with --seed). | |
| 14 | − | // | |
| 15 | − | // Model access, from the environment, passed into each container: | |
| 16 | − | // ANTHROPIC_API_KEY a provider key, or | |
| 17 | − | // ANTHROPIC_BASE_URL + ANTHROPIC_API_KEY g1t's model proxy (MODELS_URL/anthropic and a session token) | |
| 18 | − | // The judge reads ANTHROPIC_API_KEY itself (through ReviewBench's pi registry). | |
| 19 | − | // | |
| 20 | − | // Nothing that spends money runs without --yes. Each review is capped by | |
| 21 | − | // --budget-usd (default 5), and a run stops once --max-total-usd is spent. | |
| 22 | − | // | |
| 23 | − | // Needs docker, git, jq and bash (Git Bash on Windows) and Node 20+. | |
| 24 | − | ||
| 25 | − | import { execFileSync, spawnSync } from "node:child_process"; | |
| 26 | − | import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; | |
| 27 | − | import { dirname, join } from "node:path"; | |
| 28 | − | import { fileURLToPath } from "node:url"; | |
| 29 | − | ||
| 30 | − | const HERE = dirname(fileURLToPath(import.meta.url)); | |
| 31 | − | const ROOT = join(HERE, "..", ".."); | |
| 32 | − | const CACHE = join(HERE, ".cache"); | |
| 33 | − | const RB = join(CACHE, "ReviewBench"); | |
| 34 | − | const RUNS = join(CACHE, "runs"); | |
| 35 | − | // The ReviewBench commit these numbers are comparable against. | |
| 36 | − | const RB_COMMIT = "ceb0794a3768da6ef4a56e5311dfb4afd29e5dee"; | |
| 37 | − | const IMAGE = "g1t-reviewbench:dev"; | |
| 38 | − | // The model the official leaderboard is judged with. | |
| 39 | − | const DEFAULT_JUDGE = "claude-sonnet-5"; | |
| 40 | − | // services/runner/wrangler.jsonc AGENT_ROUTES.review | |
| 41 | − | const DEFAULT_MODEL = "claude-sonnet-5-5"; | |
| 42 | − | ||
| 43 | − | // $ per million tokens: input, output, cache read, cache write (5 minute). | |
| 44 | − | const PRICES = { | |
| 45 | − | "claude-sonnet-5-5": [2, 10, 0.2, 2.5], | |
| 46 | − | "claude-sonnet-5": [2, 10, 0.2, 2.5], | |
| 47 | − | "claude-opus-5-5": [4, 20, 0.2, 5], | |
| 48 | − | "claude-haiku-4-5": [1, 5, 0.1, 1.25], | |
| 49 | − | }; | |
| 50 | − | ||
| 51 | − | function args() { | |
| 52 | − | const [command, ...rest] = process.argv.slice(2); | |
| 53 | − | const flags = {}; | |
| 54 | − | for (let i = 0; i < rest.length; i++) { | |
| 55 | − | const name = rest[i].replace(/^--/, ""); | |
| 56 | − | const next = rest[i + 1]; | |
| 57 | − | if (next === undefined || next.startsWith("--")) flags[name] = true; | |
| 58 | − | else flags[name] = rest[++i]; | |
| 59 | − | } | |
| 60 | − | return { command, flags }; | |
| 61 | − | } | |
| 62 | − | ||
| 63 | − | const run = (cmd, argv, opts = {}) => { | |
| 64 | − | const result = spawnSync(cmd, argv, { stdio: "inherit", ...opts }); | |
| 65 | − | if (result.status !== 0) throw new Error(`${cmd} ${argv.join(" ")} exited ${result.status}`); | |
| 66 | − | }; | |
| 67 | − | ||
| 68 | − | function manifest() { | |
| 69 | − | return JSON.parse(readFileSync(join(RB, "corpus", "manifest.json"), "utf8")); | |
| 70 | − | } | |
| 71 | − | ||
| 72 | − | /** A seeded shuffle, so a sample is the same sample next week. */ | |
| 73 | − | function shuffle(items, seed) { | |
| 74 | − | let state = seed >>> 0 || 1; | |
| 75 | − | const random = () => ((state = (state * 1664525 + 1013904223) >>> 0) / 2 ** 32); | |
| 76 | − | const copy = [...items]; | |
| 77 | − | for (let i = copy.length - 1; i > 0; i--) { | |
| 78 | − | const j = Math.floor(random() * (i + 1)); | |
| 79 | − | [copy[i], copy[j]] = [copy[j], copy[i]]; | |
| 80 | − | } | |
| 81 | − | return copy; | |
| 82 | − | } | |
| 83 | − | ||
| 84 | − | /** Indices into the full manifest for --set. */ | |
| 85 | − | function select(set, seed) { | |
| 86 | − | const all = manifest(); | |
| 87 | − | if (set === "full") return all.map((_, i) => i); | |
| 88 | − | if (set === "test") { | |
| 89 | − | const test = JSON.parse(readFileSync(join(RB, "corpus", "test", "test.json"), "utf8")); | |
| 90 | − | const keys = new Set(test.map((p) => `${p.nwo}#${p.pr_number}`)); | |
| 91 | − | return all.flatMap((p, i) => (keys.has(`${p.nwo}#${p.pr_number}`) ? [i] : [])); | |
| 92 | − | } | |
| 93 | − | const m = /^sample:(\d+)$/.exec(set); | |
| 94 | − | if (!m) throw new Error(`--set is test, full or sample:N, not ${set}`); | |
| 95 | − | const n = Number(m[1]); | |
| 96 | − | // Stratified by change size, in the corpus's proportions, so a sample is | |
| 97 | − | // not all small or all huge. | |
| 98 | − | const bucket = (p) => { | |
| 99 | − | const lines = p.lines_added + p.lines_removed; | |
| 100 | − | return lines <= 200 ? 0 : lines <= 1000 ? 1 : 2; | |
| 101 | − | }; | |
| 102 | − | const strata = [[], [], []]; | |
| 103 | − | all.forEach((p, i) => strata[bucket(p)].push(i)); | |
| 104 | − | const picked = []; | |
| 105 | − | for (const stratum of strata) { | |
| 106 | − | const share = Math.round((stratum.length / all.length) * n); | |
| 107 | − | picked.push(...shuffle(stratum, seed).slice(0, share)); | |
| 108 | − | } | |
| 109 | − | return picked.slice(0, n).sort((a, b) => a - b); | |
| 110 | − | } | |
| 111 | − | ||
| 112 | − | /** | |
| 113 | − | * What one review is expected to cost. An agentic review re-reads its | |
| 114 | − | * growing context every turn, mostly from cache: turns and context grow | |
| 115 | − | * with the size of the change. Calibrate against real review runs (the | |
| 116 | − | * billing ledger records each run's cost) before trusting the absolute | |
| 117 | − | * numbers. | |
| 118 | − | */ | |
| 119 | − | function estimateOne(p, model) { | |
| 120 | − | const [, output, cacheRead, cacheWrite] = PRICES[model] ?? PRICES[DEFAULT_MODEL]; | |
| 121 | − | const lines = p.lines_added + p.lines_removed; | |
| 122 | − | const turns = Math.min(80, 12 + Math.round(Math.sqrt(lines) * 0.8) + Math.min(p.files_changed, 40) * 0.4); | |
| 123 | − | const startContext = 18_000 + Math.min(lines, 6_000) * 12; // prompt, tools, the diff when read | |
| 124 | − | const endContext = Math.min(startContext + turns * 2_500, 180_000); // files read along the way | |
| 125 | − | const meanContext = (startContext + endContext) / 2; | |
| 126 | − | // Each turn re-reads the context from cache and writes only what it added. | |
| 127 | − | const fresh = endContext; | |
| 128 | − | const cachedReads = Math.max(0, turns * meanContext - fresh); | |
| 129 | − | const outTokens = turns * 450 + 2_500; | |
| 130 | − | return (cachedReads * cacheRead + fresh * cacheWrite + outTokens * output) / 1e6; | |
| 131 | − | } | |
| 132 | − | ||
| 133 | − | /** | |
| 134 | − | * The judge: matching per file chunk plus classifying every unmatched | |
| 135 | − | * finding with read access to the repository (a few tool calls each). | |
| 136 | − | */ | |
| 137 | − | function estimateJudge(p, model, findingsPerPr = 6) { | |
| 138 | − | const [input, output, cacheRead] = PRICES[model] ?? PRICES[DEFAULT_JUDGE]; | |
| 139 | − | const matchCalls = Math.max(1, Math.ceil(findingsPerPr / 3)); | |
| 140 | − | const unmatched = findingsPerPr * 0.5; | |
| 141 | − | const tokensIn = matchCalls * 6_000 + unmatched * 5 * 12_000; | |
| 142 | − | const tokensOut = matchCalls * 1_500 + unmatched * 5 * 600; | |
| 143 | − | return (tokensIn * 0.5 * input + tokensIn * 0.5 * cacheRead + tokensOut * output) / 1e6; | |
| 144 | − | } | |
| 145 | − | ||
| 146 | − | function estimate(indices, model, judgeModel, rounds = 1) { | |
| 147 | − | const all = manifest(); | |
| 148 | − | let review = 0; | |
| 149 | − | let judge = 0; | |
| 150 | − | for (const i of indices) { | |
| 151 | − | review += estimateOne(all[i], model); | |
| 152 | − | judge += estimateJudge(all[i], judgeModel); | |
| 153 | − | } | |
| 154 | − | return { prs: indices.length, rounds, review_usd: review * rounds, judge_usd: judge * rounds, total_usd: (review + judge) * rounds }; | |
| 155 | − | } | |
| 156 | − | ||
| 157 | − | function setup() { | |
| 158 | − | mkdirSync(CACHE, { recursive: true }); | |
| 159 | − | if (!existsSync(join(RB, ".git"))) run("git", ["clone", "https://github.com/review-bench/ReviewBench.git", RB]); | |
| 160 | − | run("git", ["-C", RB, "fetch", "-q", "origin", RB_COMMIT]); | |
| 161 | − | run("git", ["-C", RB, "checkout", "-q", "--detach", RB_COMMIT]); | |
| 162 | − | run("npm", ["ci", "--no-audit", "--no-fund"], { cwd: RB, shell: process.platform === "win32" }); | |
| 163 | − | } | |
| 164 | − | ||
| 165 | − | function build() { | |
| 166 | − | run(process.execPath, [join(HERE, "prompt.mjs")]); | |
| 167 | − | const base = JSON.parse(readFileSync(join(ROOT, "services", "runner", "base.json"), "utf8")); | |
| 168 | − | run("docker", ["build", "--platform", "linux/amd64", "--build-arg", `BASE=${base.image}@${base.digest}`, "-t", IMAGE, HERE]); | |
| 169 | − | } | |
| 170 | − | ||
| 171 | − | function review(flags) { | |
| 172 | − | if (!flags.yes) throw new Error("review spends model credit; run estimate first, then pass --yes"); | |
| 173 | − | const set = flags.set ?? "sample:50"; | |
| 174 | − | const seed = Number(flags.seed ?? 1); | |
| 175 | − | const model = flags.model ?? DEFAULT_MODEL; | |
| 176 | − | const variant = flags.variant ?? "production"; | |
| 177 | − | const budget = Number(flags["budget-usd"] ?? 5); | |
| 178 | − | const maxTotal = Number(flags["max-total-usd"] ?? 150); | |
| 179 | − | const indices = select(set, seed); | |
| 180 | − | const id = flags.run ?? `${new Date().toISOString().slice(0, 10)}-${set.replace(":", "")}-${variant}-${model}`; | |
| 181 | − | const dir = join(RUNS, id); | |
| 182 | − | mkdirSync(dir, { recursive: true }); | |
| 183 | − | writeFileSync(join(dir, "run.json"), JSON.stringify({ id, set, seed, model, variant, budget_usd: budget, indices, reviewbench: RB_COMMIT, g1t: execFileSync("git", ["-C", ROOT, "rev-parse", "HEAD"]).toString().trim(), started_at: new Date().toISOString() }, null, 2)); | |
| 184 | − | const env = { ...process.env, RB_CONFIG_MODEL: model, RB_CONFIG_VARIANT: variant, RB_CONFIG_BUDGET_USD: String(budget), TRY_AGENT_WORK: join(dir, ".work") }; | |
| 185 | − | const pass = ["-e", "RB_CONFIG_MODEL", "-e", "RB_CONFIG_VARIANT", "-e", "RB_CONFIG_BUDGET_USD", "-e", "ANTHROPIC_API_KEY"]; | |
| 186 | − | if (process.env.ANTHROPIC_BASE_URL) pass.push("-e", "ANTHROPIC_BASE_URL"); | |
| 187 | − | let spent = 0; | |
| 188 | − | for (const i of indices) { | |
| 189 | − | if (spent >= maxTotal) { | |
| 190 | − | console.error(`stopped: spent $${spent.toFixed(2)} of --max-total-usd ${maxTotal}`); | |
| 191 | − | break; | |
| 192 | − | } | |
| 193 | − | spawnSync("bash", [join(RB, "scripts", "try-agent.sh"), IMAGE, "--set", "full", "--pr", String(i), ...pass], { cwd: dir, env, stdio: "inherit" }); | |
| 194 | − | spent = sidecars(dir).reduce((sum, s) => sum + (s.cost_usd ?? 0), 0); | |
| 195 | − | } | |
| 196 | − | console.log(`run ${id}: ${readdirSync(join(dir, "findings")).length} of ${indices.length} reviewed, $${spent.toFixed(2)} spent`); | |
| 197 | − | } | |
| 198 | − | ||
| 199 | − | /** What production would have posted beside the findings, and the cost. */ | |
| 200 | − | function sidecars(dir) { | |
| 201 | − | const out = join(dir, ".work", "out"); | |
| 202 | − | if (!existsSync(out)) return []; | |
| 203 | − | return readdirSync(out).flatMap((key) => { | |
| 204 | − | const file = join(out, key, "findings.g1t.json"); | |
| 205 | − | return existsSync(file) ? [{ key, ...JSON.parse(readFileSync(file, "utf8")) }] : []; | |
| 206 | − | }); | |
| 207 | − | } | |
| 208 | − | ||
| 209 | − | function judge(flags) { | |
| 210 | − | if (!flags.yes) throw new Error("judging spends model credit; pass --yes"); | |
| 211 | − | const dir = join(RUNS, flags.run); | |
| 212 | − | const judgeModel = flags["judge-model"] ?? DEFAULT_JUDGE; | |
| 213 | − | run("npm", ["run", "judge", "--", "--candidate", join(dir, "findings"), "--golden", join(RB, "golden"), "--manifest", join(RB, "corpus", "manifest.json"), "--provider", "anthropic", "--model", judgeModel, "--output", join(dir, "scoring", "results.json"), "--repo-dir", join(CACHE, "judge-repos"), "--concurrency", String(flags.concurrency ?? 4)], { cwd: RB, shell: process.platform === "win32" }); | |
| 214 | − | } | |
| 215 | − | ||
| 216 | − | function report(flags) { | |
| 217 | − | const dir = join(RUNS, flags.run); | |
| 218 | − | const meta = JSON.parse(readFileSync(join(dir, "run.json"), "utf8")); | |
| 219 | − | const results = JSON.parse(readFileSync(join(dir, "scoring", "results.json"), "utf8")); | |
| 220 | − | const side = sidecars(dir); | |
| 221 | − | const cost = side.reduce((sum, s) => sum + (s.cost_usd ?? 0), 0); | |
| 222 | − | const summary = { | |
| 223 | − | run: meta.id, | |
| 224 | − | date: meta.started_at.slice(0, 10), | |
| 225 | − | g1t: meta.g1t, | |
| 226 | − | model: meta.model, | |
| 227 | − | variant: meta.variant, | |
| 228 | − | set: meta.set, | |
| 229 | − | prs: side.length, | |
| 230 | − | findings: readdirSync(join(dir, "findings")).reduce((sum, f) => sum + JSON.parse(readFileSync(join(dir, "findings", f), "utf8")).findings.length, 0), | |
| 231 | − | review_cost_usd: Number(cost.toFixed(2)), | |
| 232 | − | request_changes: side.filter((s) => s.verdict === "request_changes").length, | |
| 233 | − | dropped_outside_change: side.reduce((sum, s) => sum + (s.dropped ?? 0), 0), | |
| 234 | − | // The judge's own aggregate block, as ReviewBench writes it. | |
| 235 | − | metrics: results.metrics ?? results.summary ?? results, | |
| 236 | − | }; | |
| 237 | − | if (flags.json) { | |
| 238 | − | console.log(JSON.stringify(summary)); | |
| 239 | − | return; | |
| 240 | − | } | |
| 241 | − | console.log(`## ReviewBench: ${summary.run}\n`); | |
| 242 | − | console.log(`g1t ${summary.g1t.slice(0, 8)}, ${summary.model}, prompt ${summary.variant}, ${summary.set} (${summary.prs} PRs), review cost $${summary.review_cost_usd}\n`); | |
| 243 | − | console.log("```json\n" + JSON.stringify(summary.metrics, null, 2) + "\n```"); | |
| 244 | − | } | |
| 245 | − | ||
| 246 | − | const { command, flags } = args(); | |
| 247 | − | try { | |
| 248 | − | switch (command) { | |
| 249 | − | case "setup": | |
| 250 | − | setup(); | |
| 251 | − | break; | |
| 252 | − | case "build": | |
| 253 | − | build(); | |
| 254 | − | break; | |
| 255 | − | case "estimate": { | |
| 256 | − | const indices = select(flags.set ?? "sample:50", Number(flags.seed ?? 1)); | |
| 257 | − | const model = flags.model ?? DEFAULT_MODEL; | |
| 258 | − | const judgeModel = flags["judge-model"] ?? DEFAULT_JUDGE; | |
| 259 | − | console.log(JSON.stringify({ set: flags.set ?? "sample:50", model, judge: judgeModel, ...estimate(indices, model, judgeModel, Number(flags.rounds ?? 1)) }, (k, v) => (typeof v === "number" ? Number(v.toFixed(2)) : v), 2)); | |
| 260 | − | break; | |
| 261 | − | } | |
| 262 | − | case "review": | |
| 263 | − | review(flags); | |
| 264 | − | break; | |
| 265 | − | case "judge": | |
| 266 | − | judge(flags); | |
| 267 | − | break; | |
| 268 | − | case "report": | |
| 269 | − | report(flags); | |
| 270 | − | break; | |
| 271 | − | default: | |
| 272 | − | console.error(readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n").slice(1, 22).join("\n")); | |
| 273 | − | process.exit(2); | |
| 274 | − | } | |
| 275 | − | } catch (error) { | |
| 276 | − | console.error(String(error.message ?? error)); | |
| 277 | − | process.exit(1); | |
| 278 | − | } |
| 1454 | 1454 | #[serde(default)] | |
| 1455 | 1455 | pub labels: Option<Vec<String>>, | |
| 1456 | 1456 | /// Usernames of the people it is assigned to; replaces the whole set. | |
| 1457 | − | /// Assigning it to g1t is the runner's `run`, not this. | |
| 1458 | 1457 | #[serde(default)] | |
| 1459 | 1458 | pub assignees: Option<Vec<String>>, | |
| 1460 | 1459 | /// The number of the milestone to put it in; 0 takes it out of its |
| 390 | 390 | MCP_URL: "", | |
| 391 | 391 | DOCS_URL: "", | |
| 392 | 392 | PAGES_URL: "", | |
| 393 | − | MODELS_URL: "", | |
| 394 | 393 | PROBE_REPO: process.env.STATUS_PROBE_REPO ?? "", | |
| 395 | 394 | SUPPORT_URL: `${PUBLIC_URL}/support`, | |
| 396 | 395 | OG_IMAGE: "", |
| 25 | 25 | left in work's D1, no longer read or written; read the memory parts below | |
| 26 | 26 | as history. | |
| 27 | 27 | ||
| 28 | + | **Removed the same week:** GitHub mirroring (a repository comes across | |
| 29 | + | from GitHub by importing it once; `services/repos/src/mirror.rs` is gone), | |
| 30 | + | Boards and Roadmap, the org chart and reporting lines, get-started | |
| 31 | + | checklists, preview stacks and project dependencies, and invite-only | |
| 32 | + | registration. Wherever this review leans on mirroring as a live bridge to | |
| 33 | + | GitHub, or on any of these, read it as history. | |
| 34 | + | ||
| 28 | 35 | Labels used throughout: | |
| 29 | 36 | ||
| 30 | 37 | | Label | Meaning | |
| 773 | 773 | export type PriceBook = { | |
| 774 | 774 | prices: Price[]; | |
| 775 | 775 | changes: PriceChange[]; | |
| 776 | + | /** Retired with the models g1t no longer runs; still sent. */ | |
| 776 | 777 | modelMarginPercent: number; | |
| 777 | 778 | /** Every plan, as sold now. */ | |
| 778 | 779 | plans?: FeaturePlan[]; | |
| ⋯ | |||
| 1462 | 1463 | todayMicros: number; | |
| 1463 | 1464 | /** `PLATFORM_DAILY_SPEND_CAP_MICROS`; 0: no breaker. */ | |
| 1464 | 1465 | dailyCapMicros: number; | |
| 1465 | − | /** New hosted-model agent runs g1t would pay for are paused. */ | |
| 1466 | + | /** New workflows, checks and merge queue runs g1t would pay for are paused. */ | |
| 1466 | 1467 | tripped: boolean; | |
| 1467 | 1468 | trippedAt: string | null; | |
| 1468 | 1469 | liftedBy: string | null; | |
| 48 | 48 | .map(([id]) => id.replace(/_/g, "-")); | |
| 49 | 49 | assert.deepEqual(available("workspace"), ["alerts-webhook", "datadog", "github", "jira", "linear", "sentry", "webhooks", ...models].sort()); | |
| 50 | 50 | assert.deepEqual(available("personal"), ["github", "mcp"]); | |
| 51 | − | for (const connector of CONNECTORS) { | |
| 52 | − | if (connector.mcp) assert.equal(connector.status, "soon", `${connector.id}: nothing connects to MCP servers yet`); | |
| 53 | − | } | |
| 54 | 51 | }); | |
| 55 | 52 | ||
| 56 | 53 | test("a connector shows its personal side to you", () => { |
| 49 | 49 | | "Sends events" | |
| 50 | 50 | | "Alerts into chat" | |
| 51 | 51 | | "Sets your status" | |
| 52 | − | | "Finds a time" | |
| 53 | 52 | | "AI provider" | |
| 54 | 53 | | "Deploys" | |
| 55 | 54 | | "Syncs people"; | |
| ⋯ | |||
| 72 | 71 | personal?: { description?: string; status?: ConnectorStatus; capabilities?: ConnectorCapability[] }; | |
| 73 | 72 | /** More words search should find it by. */ | |
| 74 | 73 | keywords?: string[]; | |
| 75 | − | /** | |
| 76 | − | * The remote MCP server behind it, when the tool publishes one, and how | |
| 77 | − | * it signs in (`oauth`: the browser; `header`: a key an owner pastes; | |
| 78 | − | * `none`). Its connector is soon: see `mcp()` below. | |
| 79 | − | */ | |
| 80 | − | mcp?: { | |
| 81 | − | url: string; | |
| 82 | − | auth: "oauth" | "header" | "none"; | |
| 83 | − | /** For `header`: the header the key goes in (Authorization when unset). */ | |
| 84 | − | header?: string; | |
| 85 | − | /** What to know before connecting: where the key comes from, or that signing in needs an app g1t doesn't have yet. */ | |
| 86 | − | caveat?: string; | |
| 87 | − | }; | |
| 88 | 74 | }; | |
| 89 | 75 | ||
| 90 | 76 | /** A connector as one context (a workspace, or you) shows it. */ | |
| ⋯ | |||
| 100 | 86 | keywords: string[]; | |
| 101 | 87 | /** The other scope it can also be connected in. */ | |
| 102 | 88 | alsoIn: ConnectorScope | null; | |
| 103 | − | /** The MCP server behind it, when there is one. */ | |
| 104 | − | mcp: Connector["mcp"] | null; | |
| 105 | 89 | }; | |
| 106 | 90 | ||
| 107 | 91 | /** The categories, in the order the directory lists them. */ | |
| ⋯ | |||
| 139 | 123 | provider, | |
| 140 | 124 | keywords: ["model", "llm", ...keywords], | |
| 141 | 125 | }; | |
| 142 | − | } | |
| 143 | − | ||
| 144 | − | /** | |
| 145 | − | * A tool with a remote MCP server. Soon: Customize, where an owner | |
| 146 | − | * connected one, was retired with g1t's hosted agents, so nothing in g1t | |
| 147 | − | * connects to these servers today. The server is kept for when something | |
| 148 | − | * does; until then each person's own harness connects to it directly. | |
| 149 | − | */ | |
| 150 | − | function mcp( | |
| 151 | − | id: string, | |
| 152 | − | name: string, | |
| 153 | − | category: ConnectorCategory, | |
| 154 | − | description: string, | |
| 155 | − | server: NonNullable<Connector["mcp"]>, | |
| 156 | − | capabilities: ConnectorCapability[] = ["Agents can read", "Agents can act"], | |
| 157 | − | keywords: string[] = [], | |
| 158 | − | ): Connector { | |
| 159 | − | return { id, name, category, description, scopes: W, status: "soon", capabilities, keywords: ["mcp", ...keywords], mcp: server }; | |
| 160 | 126 | } | |
| 161 | 127 | ||
| 162 | 128 | /** A planned connector. */ | |
| ⋯ | |||
| 189 | 155 | }, | |
| 190 | 156 | keywords: ["git", "import", "sign in"], | |
| 191 | 157 | }, | |
| 192 | − | mcp("github-mcp", "GitHub (MCP)", "code", "Agents read and act on repositories, issues and pull requests on GitHub, through GitHub's MCP server.", { url: "https://api.githubcopilot.com/mcp/", auth: "header", header: "Authorization", caveat: "Paste a GitHub personal access token as Bearer <token>: GitHub's server takes one in place of a sign-in." }, ["Agents can read", "Agents can act"], ["issues", "pull requests"]), | |
| 193 | 158 | { | |
| 194 | 159 | id: "webhooks", | |
| 195 | 160 | name: "Webhooks", | |
| ⋯ | |||
| 231 | 196 | }, | |
| 232 | 197 | keywords: ["tracker", "tickets"], | |
| 233 | 198 | }, | |
| 234 | − | mcp("linear-mcp", "Linear (MCP)", "issues", "Agents search, create and update issues, projects and cycles in Linear, through Linear's MCP server.", { url: "https://mcp.linear.app/mcp", auth: "oauth" }, ["Agents can read", "Agents can act"], ["tracker", "issues"]), | |
| 235 | 199 | { | |
| 236 | 200 | id: "jira", | |
| 237 | 201 | name: "Jira", | |
| ⋯ | |||
| 244 | 208 | provider: "jira", | |
| 245 | 209 | keywords: ["atlassian", "tracker", "tickets"], | |
| 246 | 210 | }, | |
| 247 | − | mcp("asana", "Asana", "issues", "Agents read, create and update tasks and projects in Asana, through Asana's MCP server.", { url: "https://mcp.asana.com/v2/mcp", auth: "oauth", caveat: "Asana signs in only apps registered with it, and g1t isn't one yet: signing in here fails until it is." }, ["Agents can read", "Agents can act"], ["tasks", "projects"]), | |
| 248 | 211 | soon("shortcut", "Shortcut", "issues", W, "Stories agents read when work mentions them, updated when the work lands.", ["Agents can read", "Writes back"]), | |
| 249 | 212 | soon("clickup", "ClickUp", "issues", W, "Tasks agents can read and import, kept in step with their pull requests.", ["Agents can read", "Writes back"]), | |
| 250 | 213 | soon("trello", "Trello", "issues", W, "Cards become issues, and move along their board as the work does.", ["Opens issues", "Writes back"]), | |
| 251 | 214 | ||
| 252 | 215 | // Chat & meetings ------------------------------------------------------- | |
| 253 | − | soon("slack", "Slack", "chat", BOTH, "Alerts and agent updates in the channels your team already reads, and agents to talk to there.", ["Alerts into chat", "Agents can act"], { | |
| 216 | + | soon("slack", "Slack", "chat", BOTH, "Alerts and updates in the channels your team already reads.", ["Alerts into chat"], { | |
| 254 | 217 | description: "Your status in g1t follows the one you set in Slack, and the other way round.", | |
| 255 | 218 | capabilities: ["Sets your status"], | |
| 256 | 219 | }), | |
| 257 | − | soon("microsoft-teams", "Microsoft Teams", "chat", BOTH, "Alerts and agent updates posted to Teams channels, with agents to talk to there.", ["Alerts into chat", "Agents can act"], { | |
| 220 | + | soon("microsoft-teams", "Microsoft Teams", "chat", BOTH, "Alerts and updates posted to Teams channels.", ["Alerts into chat"], { | |
| 258 | 221 | description: "Your status in g1t follows your Teams presence.", | |
| 259 | 222 | capabilities: ["Sets your status"], | |
| 260 | 223 | }), | |
| 261 | − | soon("discord", "Discord", "chat", W, "Releases, alerts and agent updates posted to a Discord server.", ["Alerts into chat"]), | |
| 224 | + | soon("discord", "Discord", "chat", W, "Releases, alerts and updates posted to a Discord server.", ["Alerts into chat"]), | |
| 262 | 225 | soon("zoom", "Zoom", "chat", BOTH, "Meeting notes and recordings agents can read when work refers to them.", ["Agents can read"], { | |
| 263 | 226 | description: "Sets your status to On a call while you are in a Zoom meeting.", | |
| 264 | 227 | capabilities: ["Sets your status"], | |
| ⋯ | |||
| 266 | 229 | soon("google-meet", "Google Meet", "chat", P, "Sets your status to On a call while you are in a meeting, and starts a call from any channel.", ["Sets your status"]), | |
| 267 | 230 | ||
| 268 | 231 | // Calendar & email ------------------------------------------------------ | |
| 269 | − | soon("google-calendar", "Google Calendar", "calendar", P, "Sets your status to In a meeting while you are in one; agents can find a time that suits everyone.", ["Sets your status", "Finds a time"]), | |
| 270 | − | soon("outlook-calendar", "Outlook Calendar", "calendar", P, "Sets your status to In a meeting while you are in one; agents can find a time that suits everyone.", ["Sets your status", "Finds a time"]), | |
| 271 | − | soon("gmail", "Gmail", "calendar", P, "Forward a thread to an agent, or turn an email into an issue, from your own inbox.", ["Opens issues", "Agents can read"]), | |
| 272 | − | soon("outlook-mail", "Outlook Mail", "calendar", P, "Forward a thread to an agent, or turn an email into an issue, from your own inbox.", ["Opens issues", "Agents can read"]), | |
| 232 | + | soon("google-calendar", "Google Calendar", "calendar", P, "Sets your status to In a meeting while you are in one.", ["Sets your status"]), | |
| 233 | + | soon("outlook-calendar", "Outlook Calendar", "calendar", P, "Sets your status to In a meeting while you are in one.", ["Sets your status"]), | |
| 234 | + | soon("gmail", "Gmail", "calendar", P, "Turn an email into an issue, from your own inbox.", ["Opens issues"]), | |
| 235 | + | soon("outlook-mail", "Outlook Mail", "calendar", P, "Turn an email into an issue, from your own inbox.", ["Opens issues"]), | |
| 273 | 236 | ||
| 274 | 237 | // Docs & knowledge ------------------------------------------------------ | |
| 275 | − | mcp("notion", "Notion", "docs", "Agents search, read and write Notion pages and databases, through Notion's MCP server.", { url: "https://mcp.notion.com/mcp", auth: "oauth" }, ["Agents can read", "Agents can act"], ["pages", "wiki", "databases"]), | |
| 276 | − | mcp("atlassian", "Atlassian (Jira and Confluence)", "docs", "Agents search and update Jira issues and Confluence pages, through Atlassian's MCP server.", { url: "https://mcp.atlassian.com/v1/mcp", auth: "oauth" }, ["Agents can read", "Agents can act"], ["jira", "confluence", "tickets", "wiki"]), | |
| 277 | 238 | soon("google-docs", "Google Docs", "docs", BOTH, "Agents read the specs and plans linked from issues and chat.", ["Agents can read"], { | |
| 278 | 239 | description: "Docs you share in chat open for agents as well as people.", | |
| 279 | 240 | capabilities: ["Agents can read"], | |
| 280 | 241 | }), | |
| 281 | − | mcp("zapier", "Zapier", "data", "Agents run the actions you expose from Zapier, across thousands of apps, through Zapier's MCP server.", { url: "https://mcp.zapier.com/api/mcp/mcp", auth: "header", header: "Authorization", caveat: "Paste the token from your Zapier MCP settings as Bearer <token>." }, ["Agents can act"], ["automation", "apps"]), | |
| 282 | 242 | soon("coda", "Coda", "docs", W, "Agents read the docs and tables linked from issues and chat.", ["Agents can read"]), | |
| 283 | 243 | ||
| 284 | 244 | // Monitoring & incidents ------------------------------------------------ | |
| ⋯ | |||
| 318 | 278 | provider: "webhook", | |
| 319 | 279 | keywords: ["incoming", "http", "alerts", "pagerduty", "grafana"], | |
| 320 | 280 | }, | |
| 321 | − | mcp("sentry-mcp", "Sentry (MCP)", "monitoring", "Agents search issues, read stack traces and look up errors in Sentry, through Sentry's MCP server.", { url: "https://mcp.sentry.dev/mcp", auth: "oauth" }, ["Agents can read", "Agents can act"], ["errors", "issues"]), | |
| 322 | − | soon("pagerduty", "PagerDuty", "monitoring", BOTH, "Incidents open issues an agent can start on, and resolve when the fix lands.", ["Opens issues", "Writes back"], { | |
| 281 | + | soon("pagerduty", "PagerDuty", "monitoring", BOTH, "Incidents open issues, and resolve when the fix lands.", ["Opens issues", "Writes back"], { | |
| 323 | 282 | description: "Sets your status to On call while you are on call.", | |
| 324 | 283 | capabilities: ["Sets your status"], | |
| 325 | 284 | }), | |
| 326 | 285 | soon("opsgenie", "Opsgenie", "monitoring", W, "Alerts open issues, and close when the fix lands.", ["Opens issues", "Writes back"]), | |
| 327 | − | soon("incident-io", "incident.io", "monitoring", W, "Incidents get a channel in g1t, with agents to dig through what changed.", ["Opens issues", "Alerts into chat"]), | |
| 286 | + | soon("incident-io", "incident.io", "monitoring", W, "Incidents get a channel in g1t, with what changed alongside.", ["Opens issues", "Alerts into chat"]), | |
| 328 | 287 | soon("grafana", "Grafana", "monitoring", W, "Alert rules open issues, and agents read the dashboards they link to.", ["Opens issues", "Agents can read"]), | |
| 329 | 288 | soon("new-relic", "New Relic", "monitoring", W, "Alerts open issues, with the traces agents need to find the cause.", ["Opens issues", "Agents can read"]), | |
| 330 | 289 | soon("honeycomb", "Honeycomb", "monitoring", W, "Triggers open issues, and agents query traces while they work.", ["Opens issues", "Agents can read"]), | |
| 331 | 290 | soon("better-stack", "Better Stack", "monitoring", W, "Uptime incidents and log alerts open issues.", ["Opens issues"]), | |
| 332 | 291 | ||
| 333 | 292 | // Cloud & deploy -------------------------------------------------------- | |
| 334 | − | mcp("cloudflare", "Cloudflare", "cloud", "Agents read your Cloudflare account's Workers, storage and settings, and look things up in its docs, through Cloudflare's MCP servers.", { url: "https://bindings.mcp.cloudflare.com/mcp", auth: "oauth" }, ["Agents can read", "Agents can act"], ["workers", "r2", "d1", "dns"]), | |
| 335 | − | mcp("cloudflare-docs", "Cloudflare docs", "docs", "Agents search Cloudflare's documentation while they work.", { url: "https://docs.mcp.cloudflare.com/mcp", auth: "none" }, ["Agents can read"], ["documentation"]), | |
| 336 | 293 | soon("aws", "Amazon Web Services", "cloud", W, "Workflows and deployments reach AWS with short-lived credentials, no stored keys.", ["Deploys"]), | |
| 337 | 294 | soon("google-cloud", "Google Cloud", "cloud", W, "Workflows and deployments reach Google Cloud with short-lived credentials.", ["Deploys"]), | |
| 338 | 295 | soon("azure", "Microsoft Azure", "cloud", W, "Workflows and deployments reach Azure with short-lived credentials.", ["Deploys"]), | |
| 339 | − | mcp("vercel", "Vercel", "cloud", "Agents read your Vercel projects, deployments and logs, through Vercel's MCP server.", { url: "https://mcp.vercel.com", auth: "oauth" }, ["Agents can read", "Agents can act"], ["deployments", "previews"]), | |
| 340 | 296 | soon("netlify", "Netlify", "cloud", W, "Deploy previews as checks on pull requests.", ["Deploys", "Agents can read"]), | |
| 341 | 297 | soon("fly-io", "Fly.io", "cloud", W, "Deploy apps from workflows, and roll back from the deployment's page.", ["Deploys"]), | |
| 342 | 298 | soon("kubernetes", "Kubernetes", "cloud", W, "Roll out to your own clusters from workflows, with each rollout's status on g1t.", ["Deploys"]), | |
| ⋯ | |||
| 369 | 325 | }, | |
| 370 | 326 | soon("amazon-bedrock", "Amazon Bedrock", "ai", W, "Models in your own AWS account, billed there.", ["AI provider"]), | |
| 371 | 327 | soon("vertex-ai", "Google Vertex AI", "ai", W, "Models in your own Google Cloud project, billed there.", ["AI provider"]), | |
| 372 | − | mcp("hugging-face", "Hugging Face", "ai", "Agents search models, datasets, papers and Spaces on the Hub, through Hugging Face's MCP server.", { url: "https://huggingface.co/mcp", auth: "oauth" }, ["Agents can read"], ["models", "datasets", "hub"]), | |
| 373 | − | mcp("context7", "Context7", "docs", "Agents read current documentation for the libraries a codebase uses, through Context7's MCP server.", { url: "https://mcp.context7.com/mcp", auth: "none" }, ["Agents can read"], ["documentation", "libraries"]), | |
| 374 | − | mcp("microsoft-learn", "Microsoft Learn", "docs", "Agents search Microsoft's documentation for Azure, .NET and Windows, through Microsoft Learn's MCP server.", { url: "https://learn.microsoft.com/api/mcp", auth: "none" }, ["Agents can read"], ["azure", "dotnet", "documentation"]), | |
| 375 | − | mcp("deepwiki", "DeepWiki", "docs", "Agents read generated documentation for public repositories, through DeepWiki's MCP server.", { url: "https://mcp.deepwiki.com/mcp", auth: "none" }, ["Agents can read"], ["repositories", "documentation"]), | |
| 376 | 328 | ||
| 377 | 329 | // Data & analytics ------------------------------------------------------ | |
| 378 | 330 | soon("postgresql", "PostgreSQL", "data", W, "Agents read your schema, and run read-only queries while they investigate.", ["Agents can read"]), | |
| 379 | 331 | soon("snowflake", "Snowflake", "data", W, "Agents answer questions from your warehouse with read-only queries.", ["Agents can read"]), | |
| 380 | 332 | soon("bigquery", "BigQuery", "data", W, "Agents answer questions from your datasets with read-only queries.", ["Agents can read"]), | |
| 381 | − | mcp("posthog", "PostHog", "data", "Agents read product analytics, feature flags and errors in PostHog, through PostHog's MCP server.", { url: "https://mcp.posthog.com/mcp", auth: "oauth" }, ["Agents can read", "Agents can act"], ["analytics", "feature flags"]), | |
| 382 | − | mcp("supabase", "Supabase", "data", "Agents read your Supabase projects, schemas and logs, and run queries, through Supabase's MCP server.", { url: "https://mcp.supabase.com/mcp", auth: "oauth" }, ["Agents can read", "Agents can act"], ["postgres", "database"]), | |
| 383 | − | mcp("neon", "Neon", "data", "Agents read and manage Neon Postgres projects and branches, through Neon's MCP server.", { url: "https://mcp.neon.tech/mcp", auth: "oauth" }, ["Agents can read", "Agents can act"], ["postgres", "database"]), | |
| 384 | 333 | soon("amplitude", "Amplitude", "data", W, "Agents read the charts that work refers to.", ["Agents can read"]), | |
| 385 | 334 | soon("segment", "Segment", "data", W, "Agents read your tracking plan, and check new events against it.", ["Agents can read"]), | |
| 386 | 335 | ||
| 387 | 336 | // CRM & support --------------------------------------------------------- | |
| 388 | 337 | soon("zendesk", "Zendesk", "crm", W, "Turn a ticket into an issue, and tell the customer when the fix ships.", ["Opens issues", "Writes back"]), | |
| 389 | − | mcp("intercom", "Intercom", "crm", "Agents search conversations, contacts and companies in Intercom, through Intercom's MCP server.", { url: "https://mcp.intercom.com/mcp", auth: "oauth" }, ["Agents can read", "Agents can act"], ["support", "conversations", "customers"]), | |
| 390 | − | mcp("hubspot", "HubSpot", "crm", "Agents read contacts, companies and deals in HubSpot, through HubSpot's MCP server.", { url: "https://mcp.hubspot.com/anthropic", auth: "oauth", caveat: "HubSpot signs in only apps registered with it, and g1t isn't one yet: signing in here fails until it is." }, ["Agents can read", "Agents can act"], ["crm", "contacts", "deals"]), | |
| 391 | 338 | soon("salesforce", "Salesforce", "crm", W, "Agents read the account and case behind a request.", ["Agents can read"]), | |
| 392 | − | mcp("stripe", "Stripe", "crm", "Agents read customers, payments and subscriptions, and look things up in Stripe's docs, through Stripe's MCP server.", { url: "https://mcp.stripe.com", auth: "oauth" }, ["Agents can read", "Agents can act"], ["payments", "billing", "customers"]), | |
| 393 | − | mcp("paypal", "PayPal", "crm", "Agents read and manage orders, invoices and subscriptions, through PayPal's MCP server.", { url: "https://mcp.paypal.com/mcp", auth: "oauth" }, ["Agents can read", "Agents can act"], ["payments", "invoices"]), | |
| 394 | 339 | ||
| 395 | 340 | // Design ---------------------------------------------------------------- | |
| 396 | − | mcp("figma", "Figma", "design", "Agents read designs, components and variables from your Figma files, and build to them, through Figma's MCP server.", { url: "https://mcp.figma.com/mcp", auth: "oauth" }, ["Agents can read"], ["design", "frames", "components"]), | |
| 397 | − | mcp("canva", "Canva", "design", "Agents search, create and export Canva designs, through Canva's MCP server.", { url: "https://mcp.canva.com/mcp", auth: "oauth" }, ["Agents can read", "Agents can act"], ["design", "slides"]), | |
| 398 | 341 | soon("miro", "Miro", "design", W, "Agents read the boards linked from issues and chat.", ["Agents can read"]), | |
| 399 | 342 | soon("storybook", "Storybook", "design", W, "Each pull request's stories as a check, with what changed.", ["Agents can read"]), | |
| 400 | 343 | ||
| ⋯ | |||
| 446 | 389 | provider: connector.provider ?? null, | |
| 447 | 390 | keywords: connector.keywords ?? [], | |
| 448 | 391 | alsoIn: connector.scopes.includes(other) ? other : null, | |
| 449 | − | mcp: connector.mcp ?? null, | |
| 450 | 392 | }; | |
| 451 | 393 | } | |
| 452 | 394 | ||
| 597 | 597 | /** One file of a project's docs, for a viewer who can read the repository; not found otherwise. */ | |
| 598 | 598 | repoPage(workspace: string, viewer: User, repo: string, path: string): Promise<Result<DocRepoPage>>; | |
| 599 | 599 | /** | |
| 600 | − | * Indexes the workspace's pages and projects' docs for agents' recall | |
| 600 | + | * Indexes the workspace's pages and projects' docs for search | |
| 601 | 601 | * again, in batches in the background. Workspace owners. True when a run | |
| 602 | 602 | * started (false: one is already going). | |
| 603 | 603 | */ |
| 18 | 18 | * push, a vulnerability report), shown like a mention; the rest of the | |
| 19 | 19 | * security items are ordinary `inbox` ones. | |
| 20 | 20 | */ | |
| 21 | − | export type NotificationKind = "dm" | "mention" | "thread_reply" | "inbox" | "approval" | "security"; | |
| 21 | + | export type NotificationKind = | |
| 22 | + | | "dm" | |
| 23 | + | | "mention" | |
| 24 | + | | "thread_reply" | |
| 25 | + | | "inbox" | |
| 26 | + | /** Retired with g1t's own agents (a session at its cap); kept so stored items and preferences still read. */ | |
| 27 | + | | "approval" | |
| 28 | + | | "security"; | |
| 22 | 29 | ||
| 23 | 30 | export const NOTIFICATION_KINDS: readonly NotificationKind[] = ["dm", "mention", "thread_reply", "inbox", "approval", "security"]; | |
| 24 | 31 | ||
| ⋯ | |||
| 51 | 58 | /** The thread it is in, for a reply: quick replies go there. */ | |
| 52 | 59 | thread_root?: string | null; | |
| 53 | 60 | /** | |
| 54 | − | * The chat card it is about, when that card has something to press: a | |
| 55 | − | * session at its cap (Approve more, Stop, Open), a draft issue (File | |
| 56 | − | * issue, Discard). The toast and the notifications panel show these | |
| 61 | + | * The chat card it is about, when that card has something to press, | |
| 62 | + | * such as a draft issue (File issue, Discard). The toast and the notifications panel show these | |
| 57 | 63 | * actions, and pressing one goes to the site's `card_action`, exactly as | |
| 58 | 64 | * on the card itself. | |
| 59 | 65 | */ | |
| 650 | 650 | /** The branch to merge into: the default branch when left out. */ | |
| 651 | 651 | base?: string; | |
| 652 | 652 | /** | |
| 653 | − | * Opened from a branch as a draft, still being worked on: it can't merge, | |
| 654 | − | * and agents' review routines wait, until it is marked ready. | |
| 653 | + | * Opened from a branch as a draft, still being worked on: it can't merge | |
| 654 | + | * until it is marked ready. | |
| 655 | 655 | */ | |
| 656 | 656 | draft?: boolean; | |
| 657 | 657 | }; |
| 41 | 41 | ||
| 42 | 42 | /** | |
| 43 | 43 | * The Workers that read D1 or call services that do, by config: what | |
| 44 | − | * `apply` sets the placement of. The rest (pages, models, og, runner, | |
| 44 | + | * `apply` sets the placement of. The rest (pages, og, runner, | |
| 45 | 45 | * status, docs) serve from the edge or have no data of their own. | |
| 46 | 46 | */ | |
| 47 | 47 | export const PLACED = [ | |
| ⋯ | |||
| 50 | 50 | "apps/sudo/wrangler.jsonc", | |
| 51 | 51 | "services/actions/wrangler.jsonc", | |
| 52 | 52 | "services/billing/wrangler.jsonc", | |
| 53 | − | "services/context/wrangler.jsonc", | |
| 54 | 53 | "services/deployments/wrangler.jsonc", | |
| 55 | 54 | "services/events/wrangler.jsonc", | |
| 56 | 55 | "services/identity/wrangler.jsonc", | |
| 2724 | 2724 | /** | |
| 2725 | 2725 | * Previews of a branch that is gone. A pull request's preview comes down | |
| 2726 | 2726 | * when it closes or merges; a branch's preview (built by a project's | |
| 2727 | − | * deploy workflow, or by the preview stacks projects' dependencies once | |
| 2728 | − | * built) has no pull request, so it comes down once its branch is | |
| 2729 | − | * deleted, here for any whose deletion was missed. A deleted | |
| 2727 | + | * deploy workflow) has no pull request, so it comes down once its branch | |
| 2728 | + | * is deleted, here for any whose deletion was missed. A deleted | |
| 2730 | 2729 | * workspace's stay paused for a restore. | |
| 2731 | 2730 | */ | |
| 2732 | 2731 | private async takeDownUnheld(): Promise<void> { |