flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

Fast pages, required checks on the branch, self-hosted runners, honest incidents

- Speed: placement off, one round of calls per page, slow panels stream in behind skeletons, read replicas through D1 sessions, a cached shell, cached public pages, and Server-Timing everywhere. - Required checks: workflows report checks and branch protection picks the required ones; the same gate for people and agents; Add CI opens a starter workflow. - Self-hosted runners: g1t-runner, registration, groups, runs-on: self-hosted, agent work on your machines, and $0 runtime. - Runner images split into a rarely rebuilt base and a thin layer; sandboxes clone shallow; the Actions cache moves to R2 with larger entries; bigger instance sizes. - Workflow-only egress domains, and failure() that matches GitHub Actions for skipped needs. - Incidents: streaks reset on recovery, slow is not down, blips dismiss themselves, deploys open a quiet window, and times show in your zone. - Projects follow their repository's description; blob pages link and highlight lines; tabs get padding.

syntaqxcommitted Parent64f9fb6Browse files
286 files+1710−2810/286 viewed
+33−9
88 # core, edge, front the units of each stage, in jobs that share a build;
99 # a stage starts only when the one before it succeeded
1010 #
11−# Needs the repository secret CLOUDFLARE_API_TOKEN (a Production row) and
12−# the variable CLOUDFLARE_ACCOUNT_ID, and api.cloudflare.com among the
13−# project's allowed domains (Settings, Guardrails). See docs/DEPLOYING.md.
11+# Needs the repository secret CLOUDFLARE_API_TOKEN (a Production row), the
12+# variable CLOUDFLARE_ACCOUNT_ID, and api.cloudflare.com among the project's
13+# workflow-only domains for deploy.yml in production (Settings, Guardrails),
14+# and registry.cloudflare.com there too, to find the runner's image. See
15+# docs/DEPLOYING.md.
1416 name: Deploy
1517
1618 on:
108110 core:
109111 name: core (${{ matrix.group }})
110112 needs: [plan, migrate]
111− if: ${{ !cancelled() && needs.plan.result == 'success' && (needs.migrate.result == 'success' || needs.migrate.result == 'skipped') && needs.plan.outputs.has_core == 'true' && inputs.dry_run != true }}
112− runs-on: ubuntu-latest
113+ # Runs when nothing before it failed: a migrate job skipped for having
114+ # nothing to apply is not a failure.
115+ if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_core == 'true' && inputs.dry_run != true }}
116+ # Rust builds get 4 vCPUs; everything else the standard machine.
117+ runs-on: ${{ matrix.rust && 'g1t-4core' || 'ubuntu-latest' }}
113118 environment: production
114119 timeout-minutes: 60
115120 strategy:
146151 path: ~/.cargo/registry/cache
147152 key: cargo-crates-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
148153 restore-keys: cargo-crates-${{ runner.os }}-
154+ # The compiled dependencies of this job's units, for wasm32 and the
155+ # build scripts and proc macros they run. The workspace's own crates
156+ # are compiled again whatever is cached (a checkout's sources are
157+ # newer), so an entry is saved only when the dependencies change: a
158+ # new Cargo.lock, or a new base image (base.json names its Rust).
159+ # Otherwise the nearest earlier entry, of any group, is a start.
160+ - name: Cache the Cargo target
161+ if: ${{ matrix.rust }}
162+ uses: actions/cache@v4
163+ with:
164+ path: |
165+ target/release
166+ target/wasm32-unknown-unknown/release
167+ !target/**/incremental
168+ !target/**/*.wasm
169+ key: cargo-target-${{ runner.os }}-${{ matrix.group }}-${{ hashFiles('Cargo.lock', 'services/runner/base.json') }}
170+ restore-keys: |
171+ cargo-target-${{ runner.os }}-${{ matrix.group }}-
172+ cargo-target-${{ runner.os }}-
149173 - name: Install
150174 run: node scripts/deploy.mjs install --only "${{ matrix.units }}"
151175 - name: Deploy ${{ matrix.units }}
156180 edge:
157181 name: edge (${{ matrix.group }})
158182 needs: [plan, migrate, core]
159− if: ${{ !cancelled() && needs.plan.result == 'success' && (needs.migrate.result == 'success' || needs.migrate.result == 'skipped') && (needs.core.result == 'success' || needs.core.result == 'skipped') && needs.plan.outputs.has_edge == 'true' && inputs.dry_run != true }}
160− runs-on: ubuntu-latest
183+ if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_edge == 'true' && inputs.dry_run != true }}
184+ runs-on: ${{ matrix.rust && 'g1t-4core' || 'ubuntu-latest' }}
161185 environment: production
162186 timeout-minutes: 60
163187 strategy:
169193 front:
170194 name: front (${{ matrix.group }})
171195 needs: [plan, migrate, core, edge]
172− if: ${{ !cancelled() && needs.plan.result == 'success' && (needs.migrate.result == 'success' || needs.migrate.result == 'skipped') && (needs.core.result == 'success' || needs.core.result == 'skipped') && (needs.edge.result == 'success' || needs.edge.result == 'skipped') && needs.plan.outputs.has_front == 'true' && inputs.dry_run != true }}
173− runs-on: ubuntu-latest
196+ if: ${{ !failure() && !cancelled() && needs.plan.outputs.has_front == 'true' && inputs.dry_run != true }}
197+ runs-on: ${{ matrix.rust && 'g1t-4core' || 'ubuntu-latest' }}
174198 environment: production
175199 timeout-minutes: 60
176200 strategy:
+87−0
1+# Rebuilds the base image of g1t's sandboxes (services/runner/base/Dockerfile:
2+# the OS, toolchains and the Claude Code CLI), pushes it to Cloudflare's
3+# registry, and opens a pull request that records it in
4+# services/runner/base.json. Merging that pull request is what rolls it out:
5+# the next deploy builds the runner's image on it, in seconds.
6+#
7+# Weekly, for security updates and new stable toolchains; when the base's
8+# folder changes on main (a change that forgot to rebuild it); and by hand.
9+#
10+# It needs Docker, which g1t's own sandboxes do not have, so it runs on a
11+# self-hosted runner with the `docker` label. Until one is registered, run
12+# the same thing by hand on a machine with Docker:
13+#
14+# node scripts/deploy.mjs build-base
15+#
16+# and commit services/runner/base.json. docs/DEPLOYING.md explains both.
17+name: Runner base image
18+
19+on:
20+ schedule:
21+ - cron: "17 6 * * 1"
22+ push:
23+ branches: [main]
24+ paths:
25+ - services/runner/base/**
26+ workflow_dispatch:
27+ inputs:
28+ no_cache:
29+ description: "Build every layer again, ignoring the previous base"
30+ type: boolean
31+ default: false
32+
33+concurrency:
34+ group: runner-base
35+ cancel-in-progress: false
36+
37+env:
38+ CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
39+ WRANGLER_SEND_METRICS: "false"
40+
41+jobs:
42+ build:
43+ name: Build and push the base
44+ runs-on: [self-hosted, docker]
45+ environment: production
46+ timeout-minutes: 60
47+ steps:
48+ - uses: actions/checkout@v5
49+ - name: Install Wrangler
50+ run: npm ci --workspaces=false --no-audit --no-fund
51+ - name: Build and push
52+ env:
53+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
54+ CI: "true"
55+ NO_CACHE: ${{ inputs.no_cache }}
56+ run: |
57+ args=()
58+ if [ "$NO_CACHE" = "true" ]; then args+=(--no-cache); fi
59+ node scripts/deploy.mjs build-base "${args[@]}"
60+ # The runner's own image on the new base, so the deploy after the
61+ # merge finds it in the registry instead of building it.
62+ - name: Build and push the runner's image on it
63+ env:
64+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
65+ CI: "true"
66+ run: node scripts/deploy.mjs image
67+ - name: Open a pull request with base.json
68+ env:
69+ G1T_TOKEN: ${{ github.token }}
70+ REPO: ${{ github.repository }}
71+ run: |
72+ if git diff --quiet -- services/runner/base.json; then
73+ echo "The base is unchanged."
74+ exit 0
75+ fi
76+ branch="runner-base/$(date -u +%Y%m%d-%H%M)"
77+ git config user.name "g1t"
78+ git config user.email "actions@g1t.sh"
79+ git checkout -b "$branch"
80+ git add services/runner/base.json
81+ git commit -m "A new base image for g1t's sandboxes"
82+ git push origin "$branch"
83+ tag=$(node -e 'console.log(require("./services/runner/base.json").image.split(":").pop())')
84+ body=$(node -e 'const b=require("./services/runner/base.json"); console.log("Built and pushed by the Runner base image workflow.\n\n| | |\n| --- | --- |\n" + Object.entries(b.versions).map(([k,v]) => `| ${k} | ${v} |`).join("\n") + `\n| size | ${(b.size_bytes/1e9).toFixed(2)} GB unpacked |`)')
85+ curl -fsS -X POST "https://api.g1t.sh/repos/$REPO/pulls" \
86+ -H "Authorization: Bearer $G1T_TOKEN" -H "Content-Type: application/json" \
87+ -d "$(node -e 'console.log(JSON.stringify({ title: `Runner base image ${process.argv[1]}`, branch: process.argv[2], body: process.argv[3] }))' "$tag" "$branch" "$body")"
+102−0
1+# Releases the self-hosted runner, g1t-runner (crates/runner): builds it for
2+# Linux, macOS and Windows on x64 and arm64, signs the release, publishes it
3+# to g1t.sh/downloads/runner/ (the g1t-downloads R2 bucket), and pushes its
4+# container image. Runners already out there update themselves to it.
5+#
6+# A release is a tag `runner-v<version>`, where the version is the one in
7+# crates/runner/Cargo.toml; or run it by hand. scripts/runner-release.mjs
8+# does the work; docs/DEPLOYING.md, "The self-hosted runner", says how to
9+# make the release key the first time.
10+name: Runner release
11+
12+on:
13+ push:
14+ tags: ["runner-v*"]
15+ workflow_dispatch:
16+
17+concurrency:
18+ group: runner-release
19+ cancel-in-progress: false
20+
21+env:
22+ CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
23+ WRANGLER_SEND_METRICS: "false"
24+
25+jobs:
26+ binaries:
27+ name: Build, sign and publish
28+ runs-on: ubuntu-latest
29+ environment: production
30+ timeout-minutes: 60
31+ steps:
32+ - uses: actions/checkout@v5
33+ - name: The tag names this version
34+ if: startsWith(github.ref, 'refs/tags/runner-v')
35+ run: |
36+ version="$(sed -n 's/^version = "\(.*\)"/\1/p' crates/runner/Cargo.toml | head -1)"
37+ [ "runner-v$version" = "${GITHUB_REF_NAME}" ] || { echo "::error::The tag is ${GITHUB_REF_NAME}, but crates/runner is $version"; exit 1; }
38+ - name: Install zig and cargo-zigbuild
39+ run: |
40+ pip install --user ziglang==0.13.0
41+ echo "$HOME/.local/bin" >> "$GITHUB_PATH"
42+ cargo install --locked cargo-zigbuild
43+ - uses: actions/cache@v4
44+ with:
45+ path: |
46+ ~/.cargo/registry
47+ target
48+ key: runner-release-${{ hashFiles('Cargo.lock') }}
49+ - name: Build every platform
50+ env:
51+ G1T_RUNNER_RELEASE_KEY: ${{ vars.G1T_RUNNER_RELEASE_KEY }}
52+ RUNNER_AGENT_IMAGE: ${{ vars.RUNNER_AGENT_IMAGE }}
53+ run: node scripts/runner-release.mjs build
54+ - name: Sign
55+ env:
56+ RUNNER_RELEASE_KEY: ${{ secrets.RUNNER_RELEASE_KEY }}
57+ G1T_RUNNER_RELEASE_KEY: ${{ vars.G1T_RUNNER_RELEASE_KEY }}
58+ run: |
59+ node scripts/runner-release.mjs sign
60+ node scripts/runner-release.mjs verify
61+ - name: Install Wrangler
62+ run: npm ci --workspaces=false --no-audit --no-fund
63+ - name: Publish to g1t.sh/downloads/runner
64+ env:
65+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
66+ run: node scripts/runner-release.mjs publish
67+ - uses: actions/upload-artifact@v4
68+ with:
69+ name: linux-binaries
70+ path: |
71+ target/runner-release/*/g1t-runner-linux-x64
72+ target/runner-release/*/g1t-runner-linux-arm64
73+
74+ image:
75+ name: Container image
76+ needs: binaries
77+ # Needs Docker, which g1t's own sandboxes do not have.
78+ runs-on: [self-hosted, docker]
79+ environment: production
80+ timeout-minutes: 30
81+ steps:
82+ - uses: actions/checkout@v5
83+ - uses: actions/download-artifact@v4
84+ with:
85+ name: linux-binaries
86+ path: release
87+ - name: Build and push for amd64 and arm64
88+ env:
89+ REGISTRY_USER: ${{ vars.RUNNER_IMAGE_REGISTRY_USER }}
90+ REGISTRY_TOKEN: ${{ secrets.RUNNER_IMAGE_REGISTRY_TOKEN }}
91+ IMAGE: ${{ vars.RUNNER_IMAGE }}
92+ run: |
93+ version="$(sed -n 's/^version = "\(.*\)"/\1/p' crates/runner/Cargo.toml | head -1)"
94+ echo "$REGISTRY_TOKEN" | docker login --username "$REGISTRY_USER" --password-stdin
95+ for arch in amd64 arm64; do
96+ mkdir -p "context-$arch"
97+ cp deploy/runner/Dockerfile "context-$arch/"
98+ name="g1t-runner-linux-$([ "$arch" = amd64 ] && echo x64 || echo arm64)"
99+ cp release/*/"$name" "context-$arch/g1t-runner"
100+ docker buildx build --platform "linux/$arch" -t "$IMAGE:$version-$arch" --push "context-$arch"
101+ done
102+ docker buildx imagetools create -t "$IMAGE:$version" -t "$IMAGE:latest" "$IMAGE:$version-amd64" "$IMAGE:$version-arm64"
+3−0
99 .dev.vars*
1010
1111 .env*
12+
13+# Generated by scripts/deploy.mjs for a runner deploy (the image by reference)
14+wrangler.deploy.json
+2−2
11 # Contributing to g1t
22
33 g1t is built the way it asks others to build: issues and pull requests on
4−[g1t.sh/flagon-io/g1t](https://g1t.sh/flagon-io/g1t), checks that prove a change
5−done, and a merge queue that keeps `main` passing.
4+[g1t.sh/flagon-io/g1t](https://g1t.sh/flagon-io/g1t), required checks that prove
5+a change works, and a merge queue that keeps `main` passing.
66
77 ## A change ships with its docs
88
+2−0
992992 "g1t-kit",
993993 "g1t-scan",
994994 "g1t-secrets",
995+ "miniz_oxide",
995996 "serde",
996997 "serde_json",
997998 "similar",
10041005 dependencies = [
10051006 "anyhow",
10061007 "base64 0.22.1",
1008+ "ed25519-dalek",
10071009 "g1t-actions",
10081010 "hex",
10091011 "serde",
+9−6
5555 integrations, so nothing needs a shared service account.
5656 - Public and private repositories, and git over HTTPS, including creating a
5757 repository by pushing to it.
58−- Issues with labels, acceptance checks and comments.
58+- Issues with labels and comments; a description can say what done means,
59+ under a Definition of done.
5960 - Pull requests with a diff and a recorded agent session: in a
6061 copy-on-write fork, which is how agents work, or from a branch pushed to
6162 the repository. Several can be made for one issue.
62−- Acceptance checks: an issue's commands are run against each pull request
63− in a clean sandbox, by g1t and not by the agent being checked, and gate
64− the merge.
63+- Checks: the repository's workflows run on every pull request, a
64+ person's or an agent's, and report a check each. The default branch
65+ names the required checks a merge needs; an agent whose change fails a
66+ check is sent back with the failing jobs' logs. A repository with no
67+ workflows gets a starter CI workflow in one click.
6568 - Review: comments on lines of a change, and approve or request-changes
6669 verdicts, from people and from agents.
6770 - Overlap: each pull request shows which others in progress change the
128131 | `services/repos` | Repository registry, contents, forks, diffs, landing, git over HTTPS. Rust. |
129132 | `services/work` | Issues, pull requests, reviews, check runs and sessions. Rust. |
130133 | `services/events` | The event bus and its log. Rust. |
131−| `services/runner` | Starts sandboxes: for g1t agents, and for acceptance checks. |
134+| `services/runner` | Starts sandboxes: for g1t agents, workflow jobs and the merge queue. |
132135 | `services/og` | Social cards at `og.g1t.sh`: a PNG per page, showing only what anyone may see. |
133−| `crates/runner` | The program inside a sandbox: runs an agent, or a set of checks, and reports back. Rust. |
136+| `crates/runner` | The program inside a sandbox: runs an agent, a workflow job or a merge queue build, and reports back. Rust. |
134137 | `crates/contracts` | Types and service interfaces for the Rust services. |
135138 | `crates/kit` | Plumbing shared by Rust services on Workers. |
136139 | `crates/sshd` | Git over SSH, bridged to Artifacts. Not deployed yet. |
+229−36
1−//! Artifacts and the cache of GitHub Actions jobs, kept in Workers KV in
2−//! chunks, with KV's own expiry: artifacts for 14 days with their run,
3−//! cache entries for 7 days with their repository.
1+//! Artifacts and the cache of GitHub Actions jobs.
2+//!
3+//! Artifacts are kept in Workers KV in chunks, with KV's own expiry: 14
4+//! days with their run. Cache entries are kept in R2 (ACTIONS_CACHE), up
5+//! to 2 GB each, uploaded in parts; the actions service lists them and
6+//! decides what is found, what fits and what is evicted
7+//! (services/actions/src/cache.rs). Entries saved in KV before the cache
8+//! moved are still found there until they expire.
9+//!
10+//! A sandbox reaches these with its job's token:
11+//!
12+//! - `GET /actions/jobs/{job}/artifacts`, `PUT|GET .../artifacts/{name}`
13+//! - `GET .../cache?key=&restore=`: the entry, streamed, its key in `x-g1t-key`
14+//! - `POST .../cache/uploads?key=&size=`: `{ id, upload, part_bytes }`
15+//! - `PUT .../cache/uploads/{id}/{part}?upload=`: one part, `{ part, etag }`
16+//! - `POST .../cache/uploads/{id}/complete?upload=` with `{ size, parts }`
17+//! - `DELETE .../cache/uploads/{id}?upload=`: gives the upload up
18+//! - `PUT .../cache?key=`: a whole entry of at most 60 MB at once (older runners)
419 //!
5−//! A sandbox reaches these with its job's token, at
6−//! `/actions/jobs/{job}/artifacts[/{name}]` and `/actions/jobs/{job}/cache`.
720 //! People download an artifact at
821 //! `/repos/{owner}/{repo}/actions/runs/{run}/artifacts/{name}`.
922
1023 use serde::{Deserialize, Serialize};
1124 use serde_json::{Value, json};
1225 use worker::kv::KvStore;
13−use worker::{Env, Request, Response, Result};
26+use worker::{Bucket, Env, Request, Response, Result, UploadedPart};
1427
28+use g1t_contracts::actions::{
29+ CACHE_PART_BYTES, CacheAbortArgs, CacheCommitArgs, CacheCommitted, CacheHit, CacheLookupArgs, CacheReservation, CacheReserveArgs,
30+};
1531 use g1t_contracts::{FailureCode, Outcome};
1632
1733 use crate::operations::Services;
2137 /// The largest artifact or cache entry, kept within a Worker's memory.
2238 const MAX_BYTES: usize = 60 * 1024 * 1024;
2339 const ARTIFACT_TTL: u64 = 14 * 24 * 60 * 60;
24−const CACHE_TTL: u64 = 7 * 24 * 60 * 60;
2540
2641 #[derive(Serialize, Deserialize)]
2742 struct Meta {
182197 None => error(404, "No such artifact."),
183198 }
184199 }
185− (_, "cache") => {
200+ (_, what) if what == "cache" || what.starts_with("cache/") => {
201+ let bucket = env.bucket("ACTIONS_CACHE")?;
202+ cache(request, &kv, &bucket, services, method, job, &token, &repo, what).await
203+ }
204+ _ => error(404, "No such endpoint."),
205+ }
206+}
207+
208+/// A part's number and the etag R2 gave it.
209+#[derive(Deserialize)]
210+struct Part {
211+ part: u16,
212+ etag: String,
213+}
214+
215+#[derive(Deserialize)]
216+struct Complete {
217+ size: u64,
218+ parts: Vec<Part>,
219+}
220+
221+/// An outcome of the actions service, or its failure as the reply it means.
222+fn refused<T>(outcome: Outcome<T>) -> std::result::Result<T, Result<Response>> {
223+ match outcome {
224+ Outcome::Ok(value) => Ok(value),
225+ Outcome::Fail(failure) => {
226+ let status = match failure.code {
227+ FailureCode::Unauthenticated => 401,
228+ FailureCode::NotFound => 404,
229+ FailureCode::Conflict => 409,
230+ FailureCode::Forbidden => 403,
231+ _ => 400,
232+ };
233+ Err(error(status, &failure.message))
234+ }
235+ }
236+}
237+
238+/// The cache: restoring, uploading in parts, and the older whole upload.
239+#[allow(clippy::too_many_arguments)]
240+async fn cache(
241+ mut request: Request,
242+ kv: &KvStore,
243+ bucket: &Bucket,
244+ services: &Services,
245+ method: &str,
246+ job: &str,
247+ token: &str,
248+ repo: &str,
249+ what: &str,
250+) -> Result<Response> {
251+ let parts: Vec<&str> = what.split('/').collect();
252+ let upload_id = query(&request, "upload").unwrap_or_default();
253+ match (method, parts.as_slice()) {
254+ ("GET", ["cache"]) => {
255+ let key = query(&request, "key").unwrap_or_default();
256+ let restore: Vec<String> =
257+ query(&request, "restore").unwrap_or_default().lines().map(str::trim).filter(|p| !p.is_empty()).map(str::to_owned).collect();
258+ let found: Outcome<Option<CacheHit>> = g1t_kit::call(
259+ &services.actions,
260+ "cache_lookup",
261+ &CacheLookupArgs { job: job.to_owned(), token: token.to_owned(), key: key.clone(), restore: restore.clone() },
262+ )
263+ .await?;
264+ let found = match refused(found) {
265+ Ok(found) => found,
266+ Err(reply) => return reply,
267+ };
268+ if let Some(hit) = found
269+ && let Some(object) = bucket.get(&hit.object).execute().await?
270+ && let Some(body) = object.body()
271+ {
272+ let mut response = Response::from_body(body.response_body()?)?;
273+ let headers = response.headers_mut();
274+ headers.set("x-g1t-key", &hit.key)?;
275+ headers.set("content-length", &object.size().to_string())?;
276+ headers.set("content-type", "application/octet-stream")?;
277+ return Ok(response);
278+ }
279+ // Entries saved in KV before the cache moved to R2.
280+ kv_lookup(kv, repo, &key, &restore).await
281+ }
282+ // Older runners send a whole entry of at most 60 MB at once.
283+ ("PUT", ["cache"]) => {
186284 let key = query(&request, "key").unwrap_or_default();
187− if key.is_empty() || key.len() > 400 {
188− return error(400, "A cache key is 1 to 400 characters.");
285+ let bytes = request.bytes().await?;
286+ if bytes.len() > MAX_BYTES {
287+ return error(413, "An entry sent at once is at most 60 MB; newer runners upload it in parts.");
189288 }
190− if method == "PUT" {
191− let base = format!("c/{repo}/{key}");
192− // A key is written once, as on GitHub.
193− if kv.get(&base).text().await?.is_some() {
194− return crate::reply(&json!({ "saved": false, "reason": "That key is already cached." }));
195− }
196− let bytes = request.bytes().await?;
197− if bytes.len() > MAX_BYTES {
198− return error(413, "Cache entries are at most 60 MB.");
289+ let reserved: Outcome<CacheReservation> = g1t_kit::call(
290+ &services.actions,
291+ "cache_reserve",
292+ &CacheReserveArgs { job: job.to_owned(), token: token.to_owned(), key, size: bytes.len() as u64 },
293+ )
294+ .await?;
295+ let reserved = match reserved {
296+ Outcome::Fail(failure) if failure.code == FailureCode::Conflict => {
297+ return crate::reply(&json!({ "saved": false, "reason": failure.message }));
199298 }
200− put(&kv, &base, &key, &bytes, CACHE_TTL).await?;
201− return crate::reply(&json!({ "saved": true }));
299+ other => match refused(other) {
300+ Ok(reserved) => reserved,
301+ Err(reply) => return reply,
302+ },
303+ };
304+ let size = bytes.len() as u64;
305+ bucket.put(&reserved.object, bytes).execute().await?;
306+ commit(bucket, services, job, token, &reserved.id, size).await?;
307+ crate::reply(&json!({ "saved": true }))
308+ }
309+ ("POST", ["cache", "uploads"]) => {
310+ let key = query(&request, "key").unwrap_or_default();
311+ let size = query(&request, "size").and_then(|s| s.parse::<u64>().ok()).unwrap_or(0);
312+ let reserved: Outcome<CacheReservation> = g1t_kit::call(
313+ &services.actions,
314+ "cache_reserve",
315+ &CacheReserveArgs { job: job.to_owned(), token: token.to_owned(), key, size },
316+ )
317+ .await?;
318+ let reserved = match refused(reserved) {
319+ Ok(reserved) => reserved,
320+ Err(reply) => return reply,
321+ };
322+ let upload = bucket.create_multipart_upload(&reserved.object).execute().await?;
323+ crate::reply(&json!({ "id": reserved.id, "upload": upload.upload_id().await, "part_bytes": CACHE_PART_BYTES }))
324+ }
325+ ("PUT", ["cache", "uploads", id, part]) => {
326+ let part = part.parse::<u16>().unwrap_or(0);
327+ if part == 0 || upload_id.is_empty() {
328+ return error(400, "A part is numbered from 1, and names its upload.");
202329 }
203− // The exact key, else the newest entry under each restore key.
204− let exact = format!("c/{repo}/{key}");
205− if let Some(bytes) = get(&kv, &exact).await? {
206− let mut response = Response::from_bytes(bytes)?;
207− response.headers_mut().set("x-g1t-key", &key)?;
208− return Ok(response);
330+ let length = request.headers().get("content-length")?.and_then(|l| l.parse::<u64>().ok()).unwrap_or(0);
331+ if length == 0 || length > CACHE_PART_BYTES {
332+ return error(413, &format!("A part is 1 to {} MB, with its length.", CACHE_PART_BYTES / 1_048_576));
209333 }
210− for prefix in query(&request, "restore").unwrap_or_default().lines().map(str::trim).filter(|p| !p.is_empty()) {
211− if let Some((base, meta)) = list(&kv, &format!("c/{repo}/{prefix}")).await?.into_iter().next()
212− && let Some(bytes) = get(&kv, &base).await?
213− {
214− let mut response = Response::from_bytes(bytes)?;
215− response.headers_mut().set("x-g1t-key", &meta.name)?;
216− return Ok(response);
217− }
334+ // The body goes to R2 as it comes, never held whole here.
335+ let Some(body) = request.inner().body() else { return error(400, "The part is empty.") };
336+ let upload = bucket.resume_multipart_upload(object_of(repo, id), &upload_id)?;
337+ let uploaded = upload.upload_part(part, body).await?;
338+ crate::reply(&json!({ "part": uploaded.part_number(), "etag": uploaded.etag() }))
339+ }
340+ ("POST", ["cache", "uploads", id, "complete"]) => {
341+ let done: Complete = match request.json().await {
342+ Ok(done) => done,
343+ Err(_) => return error(400, "Send { size, parts: [{ part, etag }] }."),
344+ };
345+ let upload = bucket.resume_multipart_upload(object_of(repo, id), &upload_id)?;
346+ let mut parts = done.parts;
347+ parts.sort_by_key(|p| p.part);
348+ if let Err(problem) = upload.complete(parts.into_iter().map(|p| UploadedPart::new(p.part, p.etag))).await {
349+ let _ = abort(services, job, token, id).await;
350+ return error(400, &format!("The upload could not be completed: {problem}"));
351+ }
352+ commit(bucket, services, job, token, id, done.size).await?;
353+ crate::reply(&json!({ "saved": true }))
354+ }
355+ ("DELETE", ["cache", "uploads", id]) => {
356+ if let Ok(upload) = bucket.resume_multipart_upload(object_of(repo, id), &upload_id) {
357+ let _ = upload.abort().await;
218358 }
219− error(404, "Nothing cached under those keys.")
359+ abort(services, job, token, id).await?;
360+ crate::reply(&json!({ "aborted": true }))
220361 }
221362 _ => error(404, "No such endpoint."),
222363 }
223364 }
224365
366+/// Where an entry is in R2: under its repository, by its id, as the
367+/// actions service named it when it was reserved.
368+fn object_of(repo: &str, id: &str) -> String {
369+ format!("c/{repo}/{id}")
370+}
371+
372+/// Marks an uploaded entry ready, and deletes what that evicted.
373+async fn commit(bucket: &Bucket, services: &Services, job: &str, token: &str, id: &str, size: u64) -> Result<()> {
374+ let committed: Outcome<CacheCommitted> = g1t_kit::call(
375+ &services.actions,
376+ "cache_commit",
377+ &CacheCommitArgs { job: job.to_owned(), token: token.to_owned(), id: id.to_owned(), size },
378+ )
379+ .await?;
380+ if let Outcome::Ok(committed) = committed
381+ && !committed.evicted.is_empty()
382+ {
383+ bucket.delete_multiple(committed.evicted.iter().map(String::as_str).collect()).await?;
384+ }
385+ Ok(())
386+}
387+
388+async fn abort(services: &Services, job: &str, token: &str, id: &str) -> Result<()> {
389+ let _: Outcome<bool> = g1t_kit::call(
390+ &services.actions,
391+ "cache_abort",
392+ &CacheAbortArgs { job: job.to_owned(), token: token.to_owned(), id: id.to_owned() },
393+ )
394+ .await?;
395+ Ok(())
396+}
397+
398+/// An entry saved in KV before the cache moved to R2, by key or restore key.
399+async fn kv_lookup(kv: &KvStore, repo: &str, key: &str, restore: &[String]) -> Result<Response> {
400+ // The exact key, else the newest entry under each restore key.
401+ if let Some(bytes) = get(kv, &format!("c/{repo}/{key}")).await? {
402+ let mut response = Response::from_bytes(bytes)?;
403+ response.headers_mut().set("x-g1t-key", key)?;
404+ return Ok(response);
405+ }
406+ for prefix in restore {
407+ if let Some((base, meta)) = list(kv, &format!("c/{repo}/{prefix}")).await?.into_iter().next()
408+ && let Some(bytes) = get(kv, &base).await?
409+ {
410+ let mut response = Response::from_bytes(bytes)?;
411+ response.headers_mut().set("x-g1t-key", &meta.name)?;
412+ return Ok(response);
413+ }
414+ }
415+ error(404, "Nothing cached under those keys.")
416+}
417+
225418 /// Someone who can see the run downloading one of its artifacts.
226419 pub async fn download(env: &Env, services: &Services, viewer: &g1t_contracts::Viewer, owner: &str, repo: &str, run: &str, name: &str) -> Result<Response> {
227420 let seen: Outcome<Value> = g1t_kit::call(
+14−2
1414 #[cfg(test)]
1515 mod responses;
1616 mod rest;
17+mod runners;
1718 mod tools;
1819
1920 use g1t_contracts::billing::FinishRunArgs;
240241 })
241242 }
242243
243−/// A sandbox reporting on its run of a pull request's acceptance checks.
244+/// A sandbox reporting on a run of an issue's commands, from before a pull
245+/// request's checks were the workflows run on it.
244246 /// The run's own token, in the body, is the credential: it was given to
245247 /// that sandbox and to nothing else.
246248 async fn report_checks(
469471 // g1t token either.
470472 if !on_mcp
471473 && let Some(rest) = path.strip_prefix("/actions/jobs/")
472− && (rest.contains("/artifacts") || rest.ends_with("/cache"))
474+ && (rest.contains("/artifacts") || rest.ends_with("/cache") || rest.contains("/cache/uploads"))
473475 {
474476 let rest = rest.to_owned();
475477 return blobs::for_job(request, env, &services, method, &rest).await;
476478 }
477479
480+ // A self-hosted runner, with a registration token or its own
481+ // credential, neither of which is a g1t access token.
482+ if method == "POST"
483+ && !on_mcp
484+ && path.starts_with("/runners/")
485+ && let Some(response) = runners::handle(&mut request, &services, &path).await?
486+ {
487+ return Ok(response);
488+ }
489+
478490 let viewer = match authenticate(&request, &services).await? {
479491 Ok(viewer) => viewer,
480492 Err(refused) => return Ok(refused),
+1−1
1414 const INSTRUCTIONS: &str = "g1t is a git forge where people and agents work through issues and pull requests. Repositories are named \"owner/name\"; issues and pull requests in one share a sequence of numbers.
1515 Tools are resources, each with an `action`: search, repository, issue, pull_request, agent, plan, memory, workflow, secret, webhook, access, workspace, account. The `action` field lists each action and the fields it needs. You see only what your token's scopes allow; a refusal names the scope it needs.
1616 Find a repository: account whoami lists your workspaces; repository list or search finds one.
17−Work on an issue: issue get (read it and the pull requests already made for it), memory recall, then pull_request create with the issue's number: you get a draft with its own fork to clone and push to. Record your reasoning with pull_request record_session as you go, push, then pull_request ready with a summary. Watch `overlaps` and `behind` on pull_request get, and read the acceptance checks' output there; push a fix if they fail.
17+Work on an issue: issue get (read it and the pull requests already made for it), memory recall, then pull_request create with the issue's number: you get a draft with its own fork to clone and push to. Record your reasoning with pull_request record_session as you go, push, then pull_request ready with a summary. Watch `overlaps` and `behind` on pull_request get, and its checks there: `statuses` from the repository's workflows and `required_checks`, which must pass before it merges. If one fails, read why with workflow get_run and job_logs, push a fix, and the checks run again.
1818 Hand work to g1t's agent: agent delegate opens an issue and starts it in one step; agent assign starts it on an existing issue. Each costs the workspace money.
1919 When you learn something the next agent needs, memory remember it (scope project or workspace). Never a secret.";
2020
+27−1
5555 Op::PurgeRepo,
5656 Op::GetRepoSettings,
5757 Op::UpdateRepoSettings,
58+ Op::ListCheckNames,
5859 Op::ListEvents,
5960 ],
6061 ),
162163 ],
163164 ),
164165 (
166+ "Runners",
167+ "Self-hosted runners: your own machines, which run your workflow jobs (and, if you choose, your agents' work) for $0 of g1t compute. They register with a short-lived token and only ever connect out.",
168+ &[
169+ Op::ListRunners,
170+ Op::CreateRunnerRegistrationToken,
171+ Op::RemoveRunner,
172+ Op::ListRunnerGroups,
173+ Op::CreateRunnerGroup,
174+ Op::UpdateRunnerGroup,
175+ Op::DeleteRunnerGroup,
176+ Op::GetRunnerSettings,
177+ Op::UpdateRunnerSettings,
178+ ],
179+ ),
180+ (
165181 "Webhooks",
166182 "Signed HTTPS requests sent to your own address as things happen, for a repository or a whole workspace.",
167183 &[
230246 Op::UpdateRepo => "Update a repository",
231247 Op::GetRepoSettings => "Get repository settings",
232248 Op::UpdateRepoSettings => "Update repository settings",
249+ Op::ListCheckNames => "List check names",
233250 Op::GetMergeQueue => "Get the merge queue",
234251 Op::MessageAgent => "Message an agent",
235252 Op::AnswerMessage => "Answer a message",
292309 Op::ListActionsVariables => "List variables",
293310 Op::SetActionsVariable => "Set a variable",
294311 Op::DeleteActionsVariable => "Delete a variable",
312+ Op::ListRunners => "List self-hosted runners",
313+ Op::ListRunnerGroups => "List runner groups",
314+ Op::GetRunnerSettings => "Get runner settings",
315+ Op::CreateRunnerRegistrationToken => "Create a runner registration token",
316+ Op::RemoveRunner => "Remove a self-hosted runner",
317+ Op::CreateRunnerGroup => "Create a runner group",
318+ Op::UpdateRunnerGroup => "Change a runner group",
319+ Op::DeleteRunnerGroup => "Delete a runner group",
320+ Op::UpdateRunnerSettings => "Change runner settings",
295321 Op::ListCollaborators => "List who has access",
296322 Op::AddCollaborator => "Add a collaborator",
297323 Op::UpdateCollaborator => "Change a collaborator's role",
828854 /// after the prefix, as `whsec_…` and `g1t_…` do.
829855 #[test]
830856 fn examples_hold_no_real_looking_secrets() {
831− let prefixes = ["whsec_", "g1t_", "sk_live_", "sk_test_", "ghp_", "github_pat_", "xoxb-", "AKIA"];
857+ let prefixes = ["whsec_", "g1t_", "g1tr_", "g1trt_", "sk_live_", "sk_test_", "ghp_", "github_pat_", "xoxb-", "AKIA"];
832858 for (line, text) in REFERENCE.lines().enumerate() {
833859 for prefix in prefixes {
834860 let mut rest = text;
+243−21
9292 PurgeRepo,
9393 GetRepoSettings,
9494 UpdateRepoSettings,
95+ ListCheckNames,
9596 GetMergeQueue,
9697 MessageAgent,
9798 AnswerMessage,
154155 ListActionsVariables,
155156 SetActionsVariable,
156157 DeleteActionsVariable,
158+ ListRunners,
159+ ListRunnerGroups,
160+ GetRunnerSettings,
161+ CreateRunnerRegistrationToken,
162+ RemoveRunner,
163+ CreateRunnerGroup,
164+ UpdateRunnerGroup,
165+ DeleteRunnerGroup,
166+ UpdateRunnerSettings,
157167 ListCollaborators,
158168 AddCollaborator,
159169 UpdateCollaborator,
190200 call(service, method, args).await
191201 }
192202
203+/// Commands given the deprecated way, as `checks` or `acceptance_checks`.
204+fn deprecated_checks(input: &Value) -> Vec<String> {
205+ let mut checks = strings(input, "checks").unwrap_or_default();
206+ checks.extend(strings(input, "acceptance_checks").unwrap_or_default());
207+ checks.retain(|check| !check.trim().is_empty());
208+ checks
209+}
210+
211+/// What the response says when `checks` was given: it still works, as
212+/// words in the issue's body, and what replaced it.
213+pub(crate) const CHECKS_DEPRECATION: &str = "checks is deprecated: commands are no longer run per issue. They were added to the issue's body under \"Definition of done\". What must pass before a pull request merges is the default branch's required checks: see update_repo_settings (required_checks).";
214+
215+fn with_deprecation(outcome: Outcome<Value>, deprecated: bool) -> Outcome<Value> {
216+ match outcome {
217+ Outcome::Ok(mut value) if deprecated && value.is_object() => {
218+ value["deprecation"] = Value::String(CHECKS_DEPRECATION.to_owned());
219+ Outcome::Ok(value)
220+ }
221+ other => other,
222+ }
223+}
224+
193225 fn text(input: &Value, key: &str) -> String {
194226 input[key].as_str().unwrap_or_default().to_owned()
195227 }
312344 properties
313345 }
314346
347+/// The inputs that say whose self-hosted runners: a repository's own, or a
348+/// workspace's.
349+fn runners_owner(properties: Value) -> Value {
350+ let mut properties = properties;
351+ properties["repo"] = json!({
352+ "type": "string",
353+ "description": "Repository as \"owner/name\", for its own runners (and, when listing, the workspace's it may use).",
354+ });
355+ properties["workspace"] = json!({
356+ "type": "string",
357+ "description": "Instead of repo: the workspace, for the runners its repositories share.",
358+ });
359+ properties
360+}
361+
315362 /// The inputs that say whose webhooks: a repository's, or a workspace's own.
316363 fn hook_owner(properties: Value) -> Value {
317364 let mut properties = properties;
351398 }
352399
353400 impl Op {
354− pub const ALL: [Op; 103] = [
401+ pub const ALL: [Op; 113] = [
355402 Op::Whoami,
356403 Op::CreateWorkspace,
357404 Op::DeleteWorkspace,
381428 Op::PurgeRepo,
382429 Op::GetRepoSettings,
383430 Op::UpdateRepoSettings,
431+ Op::ListCheckNames,
384432 Op::GetMergeQueue,
385433 Op::MessageAgent,
386434 Op::AnswerMessage,
443491 Op::ListActionsVariables,
444492 Op::SetActionsVariable,
445493 Op::DeleteActionsVariable,
494+ Op::ListRunners,
495+ Op::ListRunnerGroups,
496+ Op::GetRunnerSettings,
497+ Op::CreateRunnerRegistrationToken,
498+ Op::RemoveRunner,
499+ Op::CreateRunnerGroup,
500+ Op::UpdateRunnerGroup,
501+ Op::DeleteRunnerGroup,
502+ Op::UpdateRunnerSettings,
446503 Op::ListCollaborators,
447504 Op::AddCollaborator,
448505 Op::UpdateCollaborator,
492549 Op::RestoreRepo => "restore_repo",
493550 Op::PurgeRepo => "purge_repo",
494551 Op::GetRepoSettings => "get_repo_settings",
552+ Op::ListCheckNames => "list_check_names",
495553 Op::GetMergeQueue => "get_merge_queue",
496554 Op::MessageAgent => "message_agent",
497555 Op::AnswerMessage => "answer_message",
555613 Op::ListActionsVariables => "list_actions_variables",
556614 Op::SetActionsVariable => "set_actions_variable",
557615 Op::DeleteActionsVariable => "delete_actions_variable",
616+ Op::ListRunners => "list_runners",
617+ Op::ListRunnerGroups => "list_runner_groups",
618+ Op::GetRunnerSettings => "get_runner_settings",
619+ Op::CreateRunnerRegistrationToken => "create_runner_registration_token",
620+ Op::RemoveRunner => "remove_runner",
621+ Op::CreateRunnerGroup => "create_runner_group",
622+ Op::UpdateRunnerGroup => "update_runner_group",
623+ Op::DeleteRunnerGroup => "delete_runner_group",
624+ Op::UpdateRunnerSettings => "update_runner_settings",
558625 Op::ListCollaborators => "list_collaborators",
559626 Op::AddCollaborator => "add_collaborator",
560627 Op::UpdateCollaborator => "update_collaborator",
645712 "Move a repository to another workspace, keeping its name. You must own both workspaces, and the destination must not already have a repository of that name; a free destination takes a private repository only if its private storage has room. Everything moves with it: git data, issues, pull requests, comments, labels, workflow runs, deployments, its project, and its own secrets, variables and webhooks. Its old address keeps working: web pages, git remotes and API calls redirect to the new one until a repository is made at the old address. Usage from now on is charged to the new workspace."
646713 }
647714 Op::GetRepoSettings => {
648− "How a repository handles pull requests: the approvals a merge needs, whether failed checks can be overridden, whether a pull request must be up to date, and how g1t's agents are reviewed, revised and merged."
715+ "How a repository handles pull requests, as its default branch's protection: the checks that must pass (required_checks), the approvals a merge needs, whether required checks can be bypassed, whether a pull request must be up to date, and how g1t's agents are reviewed, revised and merged. The same rules hold for a person's pull request and an agent's."
649716 }
650717 Op::UpdateRepoSettings => {
651− "Change how a repository handles pull requests. Only the fields given are changed. Needs the Maintain role or higher."
718+ "Change how a repository handles pull requests. Only the fields given are changed; required_checks replaces the whole list. A required check is named as list_check_names gives it: a workflow's name, such as CI, or another status's context, such as g1t / deploy. Needs the Maintain role or higher."
652719 }
720+ Op::ListCheckNames => {
721+ "The check names reported on a repository's commits in the last 30 days, most recent first, with the events each was reported for: the names update_repo_settings takes in required_checks. A workflow's runs report a check named after the workflow; a check required on the default branch must be reported on a pull request's head (pull_request events) and, with the merge queue on, on its queued state (merge_group events)."
722+ }
653723 Op::MessageAgent => {
654724 "Send the agent working on a pull request a message: a correction, a hint, a change of plan. It receives it at its next step, and it is recorded in the pull request's session. The pull request's author, and anyone with the Write role or higher. An agent uses it to ask the agent on another pull request a question (kind: question) or hand it work that belongs there (kind: handoff), giving its own pull request as from_number; the answer comes back to it at its next step."
655725 }
684754 "Issues on a repository, newest first. An issue is something that should change: a bug, a feature, a question. Pull requests are made against it."
685755 }
686756 Op::GetIssue => {
687− "An issue: its description, labels and acceptance checks, its comments, and every pull request made against it with its status. If the issue is closed, resolvedBy is the number of the pull request that was merged for it. Read this before opening a pull request, to see what others have already tried."
757+ "An issue: its description (which may say what done means, under \"Definition of done\"), labels, its comments, and every pull request made against it with its status. If the issue is closed, resolved_by is the number of the pull request that was merged for it. Read this before opening a pull request, to see what others have already tried."
758+ }
759+ Op::CreateIssue => {
760+ "Open an issue on a repository. Say what done means in the body if it helps, for instance under a \"Definition of done\" heading; what must pass before a pull request for it merges is the default branch's required checks, the same for every pull request."
688761 }
689− Op::CreateIssue => "Open an issue on a repository.",
690762 Op::UpdateIssue => {
691763 "Change an issue's title, body, labels or the people it is assigned to. Only the fields given are changed; labels and assignees each replace the whole set. Its author may change their own issue; anyone else needs the Triage role or higher."
692764 }
695767 }
696768 Op::ReopenIssue => "Reopen a closed issue. Its author may reopen their own issue; anyone else needs the Triage role or higher.",
697769 Op::PlanWork => {
698− "Turn an outcome into a plan. An agent reads the repository and proposes the issues that would get there: what each changes, the checks it must pass, the files it will touch, and which must merge before which. Returns the plan's id at once; the plan takes a minute or two to write, so read it with get_plan until its status is ready. Nothing is opened until apply_plan. Needs the Write role or higher."
770+ "Turn an outcome into a plan. An agent reads the repository and proposes the issues that would get there: what each changes, what done means for it (added to its body under \"Definition of done\"), the files it will touch, and which must merge before which. Returns the plan's id at once; the plan takes a minute or two to write, so read it with get_plan until its status is ready. Nothing is opened until apply_plan. Needs the Write role or higher."
699771 }
700772 Op::GetPlan => {
701773 "A plan: the outcome asked for, its status (planning, ready, failed or applied), and the issues it proposes with their dependencies."
704776 "Open a plan's issues, each blocked by the ones it depends on. With assign, g1t agents start at once on every issue that depends on nothing, working in parallel, and on the others as what they depend on merges. keep limits it to some of the proposed issues, by their positions counting from 1. A plan is applied once. Needs the Write role or higher."
705777 }
706778 Op::AssignIssue => {
707− "Assign an issue to the g1t agent. It opens a pull request for the issue in a sandbox of its own and sees it through: the issue's acceptance checks, a review by a second agent, revision if either finds something, and catching up when main moves. Returns the pull request at once; follow its progress with get_pull_request. There is no model or agent count to choose. To put many agents to work, assign many issues. Needs the Write role or higher. In preview: only for accounts g1t agents are enabled for."
779+ "Assign an issue to the g1t agent. It opens a pull request for the issue in a sandbox of its own and sees it through: the repository's workflows run on it as its checks, a second agent reviews it, it revises when a check fails (reading the failing jobs' logs) or the review asks for changes, and it catches up when main moves. It is ready once the default branch's required checks pass and the review approves. Returns the pull request at once; follow its progress with get_pull_request. There is no model or agent count to choose. To put many agents to work, assign many issues. Needs the Write role or higher. In preview: only for accounts g1t agents are enabled for."
708780 }
709781 Op::Delegate => {
710− "Put an agent on something in one step: open an issue and assign it to the g1t agent at once. Say what you want done in plain words; give checks, commands that must pass, when you know them. Needs the Write role or higher, and nothing is opened without it. The issue is opened whatever happens next: agent.status is started (pull is the draft pull request the agent opened; follow it with get_pull_request), queued (every agent slot of the workspace is busy; it starts by itself when one frees up) or not_started, with agent.code saying why (not_paid, trial_used, limit, paused, issue_cap, billing_unavailable or no_model), agent.message saying what to do, and agent.fix_url where. There is no model or agent count to choose."
782+ "Put an agent on something in one step: open an issue and assign it to the g1t agent at once. Say what you want done in plain words, with what done means if you know it. What must pass before its pull request merges is the default branch's required checks. Needs the Write role or higher, and nothing is opened without it. The issue is opened whatever happens next: agent.status is started (pull is the draft pull request the agent opened; follow it with get_pull_request), queued (every agent slot of the workspace is busy; it starts by itself when one frees up) or not_started, with agent.code saying why (not_paid, trial_used, limit, paused, issue_cap, billing_unavailable or no_model), agent.message saying what to do, and agent.fix_url where. There is no model or agent count to choose."
711783 }
712784 Op::ListLabels => "The labels available on a repository's issues.",
713785 Op::AddComment => {
720792 "Pull requests on a repository, newest first. State open covers drafts and those ready for review; closed covers merged and closed."
721793 }
722794 Op::GetPullRequest => {
723− "A pull request's status, head commit, comments and reviews, the issue it is for, the latest run of that issue's acceptance checks with each command's output, whether it is behind the branch it would merge into, and overlaps: other pull requests in progress that change the same files. An overlap with a pull request for a different issue means the two will conflict; say so, or keep clear of those files."
795+ "A pull request's status, head commit, comments and reviews, the issue it is for, its checks (statuses: what each workflow run reported on its head, with a link to the run; get_workflow_run and get_job_logs say why one failed), required_checks (each check the default branch requires, as success, failure, pending or expected when nothing has reported it yet), whether it is behind the branch it would merge into, and overlaps: other pull requests in progress that change the same files. An overlap with a pull request for a different issue means the two will conflict; say so, or keep clear of those files."
724796 }
725797 Op::CreatePullRequest => {
726798 "Start a change. Opens a draft pull request with its own fork of the repository and returns the fork's git remote. Clone it, commit your work there, push, record your session as you go, then call mark_pull_request_ready. Give the issue it is for whenever there is one. If the change is already on a branch pushed to the repository, give that branch instead: no fork is made and the pull request is ready for review at once."
737809 "What a pull request changes: the files it touches and their line-by-line diff against the commit it started from. Use it to review a pull request or to compare several made for the same issue."
738810 }
739811 Op::MergePullRequest => {
740− "Land a pull request on the repository's main branch. Merging needs the Write role or higher, and only once it is marked ready and its acceptance checks have passed. Merging resolves the issue it was made for: the issue closes recording this pull request, and the other pull requests still in progress for that issue close as superseded. Where the repository has a merge queue, it joins the queue instead of landing at once. If main has moved since the pull request was opened, it is brought up to date first and lands when that is done; a repository that requires pull requests to be up to date refuses instead, so pull main into its fork or branch, push, and merge again. Check status in the result to see whether it has landed."
812+ "Land a pull request on the repository's main branch. Merging needs the Write role or higher, and only once it is marked ready and every check the default branch requires has passed on its head (see required_checks on get_pull_request); with ignore_checks, someone who may merge can bypass them where the repository allows it. Merging resolves the issue it was made for: the issue closes recording this pull request, and the other pull requests still in progress for that issue close as superseded. Where the repository has a merge queue, it joins the queue instead of landing at once. If main has moved since the pull request was opened, it is brought up to date first and lands when that is done; a repository that requires pull requests to be up to date refuses instead, so pull main into its fork or branch, push, and merge again. Check status in the result to see whether it has landed."
741813 }
742814 Op::ListEvents => {
743815 "The timeline of a repository: pushes, issues, pull requests, comments and session activity, newest first."
810882 }
811883 Op::SetActionsVariable => "Add or change a variable's row, as for secrets.",
812884 Op::DeleteActionsVariable => "Remove a variable: one row by `id`, or every row of the key.",
885+ Op::ListRunners => {
886+ "A workspace's self-hosted runners, or a repository's: its own and the workspace's that its runner group lets it use. Each has its `labels` (always `self-hosted`, its OS and its architecture), `status` (`online`, `busy` or `offline`), the `work` it is doing, its `version` and when it was last seen. A workspace's are seen by its members; a repository's need the Admin role on it."
887+ }
888+ Op::ListRunnerGroups => {
889+ "A workspace's runner groups: which of its repositories may use the runners in each. The default group (every repository) is where runners go when no group is named. Members only."
890+ }
891+ Op::GetRunnerSettings => {
892+ "Where a workspace's (or a repository's) g1t agent work runs, and whether pull requests from forks may use its self-hosted runners. `agents_on_self_hosted` sends agent runs, checks, reviews and the merge queue to runners with `agent_labels` instead of g1t's sandboxes. A repository's are its workspace's unless it has its own (`inherited`)."
893+ }
894+ Op::CreateRunnerRegistrationToken => {
895+ "A registration token for `g1t-runner register`, shown once. It lasts an hour and registers any number of runners until then, into `group` (the default group if none) for a workspace, or as a repository's own runners. It can do nothing else. Owners of the workspace, or admins of the repository, signed in or with a person's token; workspace tokens, G1T_TOKEN included, are refused."
896+ }
897+ Op::RemoveRunner => {
898+ "Remove a self-hosted runner: its credential stops working at once and a job it is running fails. The machine's `g1t-runner` stops on its next poll. Owners of the workspace, or admins of the repository."
899+ }
900+ Op::CreateRunnerGroup => {
901+ "Create a runner group: the repositories (by name) that may use the runners in it; empty for every repository. Owners only."
902+ }
903+ Op::UpdateRunnerGroup => "Rename a runner group, or change which repositories may use it. Owners only.",
904+ Op::DeleteRunnerGroup => "Delete a runner group. Its runners join the default group, which cannot be deleted. Owners only.",
905+ Op::UpdateRunnerSettings => {
906+ "Change where g1t agent work runs and whether pull requests from forks may use self-hosted runners, for a workspace or one repository. Left out is unchanged; `inherit` drops a repository's own settings. Allowing forks lets anyone who can open a pull request run code on your machines. Owners of the workspace, or admins of the repository."
907+ }
813908 Op::ImportIssue => {
814909 "Open an issue from a ticket in Jira or Linear, or from a Sentry issue, by its key or address. The issue is linked to it: agents read the original, and when the work lands the ticket is told. Importing the same ticket again returns the issue already made. With assign, a g1t agent starts on it."
815910 }
10351130 &["repo", "confirm"],
10361131 ),
10371132 Op::ListDeletedRepos => object(json!({ "workspace": workspace_schema() }), &["workspace"]),
1038− Op::GetRepoSettings => object(json!({ "repo": repo_schema() }), &["repo"]),
1133+ Op::GetRepoSettings | Op::ListCheckNames => object(json!({ "repo": repo_schema() }), &["repo"]),
10391134 Op::GetMergeQueue => object(json!({ "repo": repo_schema() }), &["repo"]),
10401135 Op::MessageAgent => object(
10411136 numbered(json!({
11411236 "type": "boolean",
11421237 "description": "Land a g1t agent's pull request without a person once every rule is met.",
11431238 },
1239+ "required_checks": {
1240+ "type": "array",
1241+ "items": { "type": "string" },
1242+ "description": "The checks that must pass on a pull request's head before it merges into the default branch, by name: a workflow's name (CI) or another status's context (g1t / deploy). list_check_names gives the names seen lately. Replaces the whole list; an empty list requires none.",
1243+ },
11441244 "require_up_to_date": {
11451245 "type": "boolean",
11461246 "description": "Refuse to merge a pull request that is behind the default branch. When false, merging brings it up to date first.",
11551255 },
11561256 "allow_ignoring_checks": {
11571257 "type": "boolean",
1158− "description": "Whether a member may merge although the acceptance checks did not pass.",
1258+ "description": "Whether someone who may merge can bypass required checks that have not passed, with ignore_checks.",
11591259 },
11601260 "agent_review": {
11611261 "type": "boolean",
12211321 "checks": {
12221322 "type": "array",
12231323 "items": { "type": "string" },
1224− "description": "Commands that must pass for a pull request to be accepted.",
1324+ "deprecated": true,
1325+ "description": "Deprecated. Commands are added to the body under \"Definition of done\", and the response says so in deprecation. What must pass before a pull request merges is the default branch's required checks.",
12251326 },
12261327 }),
12271328 &["repo", "title"],
12831384 "checks": {
12841385 "type": "array",
12851386 "items": { "type": "string" },
1286− "description": "Commands that must pass for its pull request to be accepted, e.g. \"npm test\".",
1387+ "deprecated": true,
1388+ "description": "Deprecated, as on create_issue: commands are added to the body under \"Definition of done\".",
12871389 },
12881390 "labels": {
12891391 "type": "array",
14001502 },
14011503 "ignore_checks": {
14021504 "type": "boolean",
1403− "description": "Merge although the acceptance checks have not passed.",
1505+ "description": "Merge although required checks have not passed, where the repository allows bypassing them (allow_ignoring_checks).",
14041506 },
14051507 })),
14061508 &["repo", "number"],
15181620 })),
15191621 &["setting"],
15201622 ),
1623+ Op::ListRunners | Op::GetRunnerSettings => object(runners_owner(json!({})), &[]),
1624+ Op::CreateRunnerRegistrationToken => object(
1625+ runners_owner(json!({
1626+ "group": { "type": "string", "description": "A workspace's runner group, by name or id, for the runners it registers. The default group if left out." },
1627+ })),
1628+ &[],
1629+ ),
1630+ Op::RemoveRunner => object(
1631+ runners_owner(json!({ "id": { "type": "string", "description": "The runner's id, from a list." } })),
1632+ &["id"],
1633+ ),
1634+ Op::ListRunnerGroups => object(json!({ "workspace": workspace_schema() }), &["workspace"]),
1635+ Op::CreateRunnerGroup | Op::UpdateRunnerGroup => object(
1636+ json!({
1637+ "workspace": workspace_schema(),
1638+ "id": { "type": "string", "description": "The group to change, from a list. Left out: a new group." },
1639+ "name": { "type": "string", "description": "What to call it." },
1640+ "repositories": {
1641+ "type": "array",
1642+ "items": { "type": "string" },
1643+ "description": "Repository names that may use its runners. Empty is every repository in the workspace.",
1644+ },
1645+ }),
1646+ if self == Op::UpdateRunnerGroup { &["workspace", "id"] } else { &["workspace", "name"] },
1647+ ),
1648+ Op::DeleteRunnerGroup => object(
1649+ json!({ "workspace": workspace_schema(), "id": { "type": "string", "description": "The group's id." } }),
1650+ &["workspace", "id"],
1651+ ),
1652+ Op::UpdateRunnerSettings => object(
1653+ runners_owner(json!({
1654+ "agents_on_self_hosted": { "type": "boolean", "description": "Run agent runs, checks, reviews and the merge queue on self-hosted runners." },
1655+ "agent_labels": {
1656+ "type": "array",
1657+ "items": { "type": "string" },
1658+ "description": "The labels a runner needs to take agent work. self-hosted is always one.",
1659+ },
1660+ "fork_pull_requests": { "type": "boolean", "description": "Let jobs of pull requests from forks run on self-hosted runners." },
1661+ "inherit": { "type": "boolean", "description": "For a repository: drop its own settings and follow its workspace's." },
1662+ })),
1663+ &[],
1664+ ),
15211665 Op::CreateWebhook => object(
15221666 hook_owner(json!({
15231667 "url": { "type": "string", "description": "An HTTPS address on the public internet." },
16591803 | Op::GetPullRequestChanges
16601804 | Op::ListEvents
16611805 | Op::GetRepoSettings
1806+ | Op::ListCheckNames
16621807 | Op::GetMergeQueue
16631808 )
16641809 }
17101855 | Op::ListActionsVariables
17111856 | Op::SetActionsVariable
17121857 | Op::DeleteActionsVariable
1858+ | Op::ListRunners
1859+ | Op::ListRunnerGroups
1860+ | Op::GetRunnerSettings
1861+ | Op::CreateRunnerRegistrationToken
1862+ | Op::RemoveRunner
1863+ | Op::CreateRunnerGroup
1864+ | Op::UpdateRunnerGroup
1865+ | Op::DeleteRunnerGroup
1866+ | Op::UpdateRunnerSettings
17131867 | Op::ListMyRepoInvitations
17141868 | Op::AcceptRepoInvitation
17151869 | Op::DeclineRepoInvitation
21222276 )
21232277 .await
21242278 }
2279+ Op::ListCheckNames => {
2280+ pass(
2281+ work,
2282+ "seen_checks",
2283+ &json!({ "repo": repo, "viewer": viewer }),
2284+ )
2285+ .await
2286+ }
21252287 Op::GetMergeQueue => {
21262288 pass(work, "queue", &json!({ "repo": repo, "viewer": viewer })).await
21272289 }
22752437 let flag = |key: &str, now: bool| input[key].as_bool().unwrap_or(now);
22762438 let settings = RepoSettings {
22772439 auto_merge: flag("auto_merge", current.auto_merge),
2440+ required_checks: strings(input, "required_checks").unwrap_or(current.required_checks.clone()),
22782441 require_up_to_date: flag("require_up_to_date", current.require_up_to_date),
22792442 required_approvals: integer(input, "required_approvals")
22802443 .unwrap_or(current.required_approvals),
23422505 }
23432506 Op::GetIssue => pass(work, "get_issue", &view()).await,
23442507 Op::CreateIssue => {
2345− pass(
2508+ let checks = deprecated_checks(input);
2509+ let opened = pass(
23462510 work,
23472511 "open_issue",
23482512 &OpenIssueArgs {
23512515 title: text(input, "title"),
23522516 body: text(input, "body"),
23532517 labels: strings(input, "labels").unwrap_or_default(),
2354− checks: strings(input, "checks").unwrap_or_default(),
2518+ checks: checks.clone(),
23552519 },
23562520 )
2357− .await
2521+ .await?;
2522+ Ok(with_deprecation(opened, !checks.is_empty()))
23582523 }
23592524 Op::UpdateIssue => {
23602525 pass(
24072572 .await
24082573 }
24092574 Op::Delegate => {
2410− pass(
2575+ let checks = deprecated_checks(input);
2576+ let delegated = pass(
24112577 runner,
24122578 "delegate",
24132579 &json!({
24162582 "title": text(input, "title"),
24172583 "body": text(input, "body"),
24182584 "labels": strings(input, "labels").unwrap_or_default(),
2419− "checks": strings(input, "checks").unwrap_or_default(),
2585+ "checks": checks,
24202586 }),
24212587 )
2422− .await
2588+ .await?;
2589+ Ok(with_deprecation(delegated, !checks.is_empty()))
24232590 }
24242591 Op::AssignIssue => {
24252592 pass(
27552922 Op::GetModelRoutes => {
27562923 pass(integrations, "routes", &json!({ "workspace": workspace(), "viewer": viewer })).await
27572924 }
2925+ Op::ListRunners
2926+ | Op::GetRunnerSettings
2927+ | Op::CreateRunnerRegistrationToken
2928+ | Op::RemoveRunner
2929+ | Op::UpdateRunnerSettings => {
2930+ // A repository's own runners, or with no repository named,
2931+ // the workspace's.
2932+ let mut args = match repo_path(input) {
2933+ Some(repo) => json!({ "repo": repo }),
2934+ None if !workspace().is_empty() => json!({ "workspace": workspace() }),
2935+ None => return failed(FailureCode::Invalid, "Name the repository as repo, or the workspace as workspace."),
2936+ };
2937+ args["actor"] = json!(actor());
2938+ let method = match self {
2939+ Op::ListRunners => "runners",
2940+ Op::GetRunnerSettings => "runner_settings",
2941+ Op::CreateRunnerRegistrationToken => "create_registration_token",
2942+ Op::RemoveRunner => "remove_runner",
2943+ _ => "set_runner_settings",
2944+ };
2945+ if let Some(group) = optional_text(input, "group") {
2946+ args["group"] = json!(group);
2947+ }
2948+ if let Some(id) = optional_text(input, "id") {
2949+ args["id"] = json!(id);
2950+ }
2951+ for key in ["agents_on_self_hosted", "fork_pull_requests", "inherit"] {
2952+ if let Some(on) = input[key].as_bool() {
2953+ args[key] = json!(on);
2954+ }
2955+ }
2956+ if let Some(labels) = strings(input, "agent_labels") {
2957+ args["agent_labels"] = json!(labels);
2958+ }
2959+ pass(actions, method, &args).await
2960+ }
2961+ Op::ListRunnerGroups => {
2962+ pass(actions, "runner_groups", &json!({ "actor": actor(), "workspace": workspace() })).await
2963+ }
2964+ Op::CreateRunnerGroup | Op::UpdateRunnerGroup => {
2965+ let mut args = json!({ "actor": actor(), "workspace": workspace() });
2966+ if self == Op::UpdateRunnerGroup {
2967+ args["id"] = json!(text(input, "id"));
2968+ }
2969+ if let Some(name) = optional_text(input, "name") {
2970+ args["name"] = json!(name);
2971+ }
2972+ if let Some(repositories) = strings(input, "repositories") {
2973+ args["repositories"] = json!(repositories);
2974+ }
2975+ pass(actions, "set_runner_group", &args).await
2976+ }
2977+ Op::DeleteRunnerGroup => {
2978+ pass(actions, "delete_runner_group", &json!({ "actor": actor(), "workspace": workspace(), "id": text(input, "id") })).await
2979+ }
27582980 Op::SetModelRoutes => {
27592981 let routes: Vec<Value> = input["routes"]
27602982 .as_array()
+344−59
690690 "get_repo_settings": {
691691 "response": {
692692 "auto_merge": false,
693+ "required_checks": [
694+ "CI"
695+ ],
693696 "require_up_to_date": false,
694697 "required_approvals": 0,
695698 "count_agent_approvals": true,
704707 },
705708 "update_repo_settings": {
706709 "request": {
710+ "required_checks": [
711+ "CI",
712+ "g1t / deploy"
713+ ],
707714 "required_approvals": 1,
708715 "count_agent_approvals": false,
709716 "merge_queue": true
710717 },
711718 "response": {
712719 "auto_merge": false,
720+ "required_checks": [
721+ "CI",
722+ "g1t / deploy"
723+ ],
713724 "require_up_to_date": false,
714725 "required_approvals": 1,
715726 "count_agent_approvals": false,
721732 "updated_by": "syntaqx",
722733 "updated_at": "2026-10-04T16:20:37.508Z"
723734 },
724− "notes": "`required_approvals` is at most 6 and `max_revisions` at most 5. See [what a repository can ask for](/guides/g1t-agents/#what-a-repository-can-ask-for). `hold_low_confidence` is on unless turned off: see [confidence](/guides/g1t-agents/#how-sure-the-agent-is)."
735+ "notes": "`required_checks` replaces the whole list: at most 20 names, each a workflow's name or another status's context, as [`list_check_names`](#list_check_names) gives them. A pull request merges only once each passes on its head; one nothing has reported holds it too. With the merge queue on, each must also pass on the queued state, so the workflows behind them need `merge_group` in their `on:`. `required_approvals` is at most 6 and `max_revisions` at most 5. See [what a repository can ask for](/guides/g1t-agents/#what-a-repository-can-ask-for). `hold_low_confidence` is on unless turned off: see [confidence](/guides/g1t-agents/#how-sure-the-agent-is)."
725736 },
737+ "list_check_names": {
738+ "response": [
739+ {
740+ "name": "CI",
741+ "events": [
742+ "pull_request",
743+ "merge_group",
744+ "push"
745+ ],
746+ "last_seen": "2026-10-06T09:14:02.118Z"
747+ },
748+ {
749+ "name": "g1t / deploy",
750+ "events": [],
751+ "last_seen": "2026-10-06T08:51:40.006Z"
752+ }
753+ ],
754+ "notes": "The names reported on the repository's commits in the last 30 days, most recent first. A workflow reports a check named after it (`CI`), for each event it ran for; another tool, such as a deployment, reports its own name (`g1t / deploy`). Empty until something has reported on a commit: add a workflow in `.g1t/workflows` first."
755+ },
726756 "list_events": {
727757 "query": {
728758 "before": "evt_01m43t2a6c9e3g7j1m5q9t3x7b"
775805 "labels": [
776806 "feature"
777807 ],
778− "checks": [
779− "cargo test"
780− ],
781808 "state": "open",
782809 "reason": null,
783810 "resolved_by": null,
807834 "body": "Take a name from the first argument; fall back to world.",
808835 "labels": [
809836 "feature"
810− ],
811− "checks": [
812− "cargo test"
813837 ]
814838 },
815839 "response": {
821845 "labels": [
822846 "feature"
823847 ],
824− "checks": [
825− "cargo test"
826− ],
827848 "state": "open",
828849 "reason": null,
829850 "resolved_by": null,
856877 "body": "Take a name from the first argument; fall back to world.",
857878 "labels": [
858879 "feature"
859− ],
860− "checks": [
861− "cargo test"
862880 ],
863881 "state": "closed",
864882 "reason": "completed",
10001018 "labels": [
10011019 "feature",
10021020 "good first issue"
1003− ],
1004− "checks": [
1005− "cargo test"
10061021 ],
10071022 "state": "open",
10081023 "reason": null,
10391054 "body": "Take a name from the first argument; fall back to world.",
10401055 "labels": [
10411056 "feature"
1042− ],
1043− "checks": [
1044− "cargo test"
10451057 ],
10461058 "state": "closed",
10471059 "reason": "not_planned",
10741086 "labels": [
10751087 "feature"
10761088 ],
1077− "checks": [
1078− "cargo test"
1079− ],
10801089 "state": "open",
10811090 "reason": null,
10821091 "resolved_by": null,
11431152 "delegate": {
11441153 "request": {
11451154 "title": "Retry webhooks with exponential backoff",
1146− "body": "Deliveries that fail are dropped today. Retry them with exponential backoff, up to six times, then mark the delivery failed.",
1147− "checks": [
1148− "npm test"
1149− ]
1155+ "body": "Deliveries that fail are dropped today. Retry them with exponential backoff, up to six times, then mark the delivery failed.\n\n## Definition of done\n\n- A delivery that fails is retried after 1, 2, 4, 8, 16 and 32 seconds.\n- After the sixth failure it is marked failed and shows on the webhook's page."
11501156 },
11511157 "response": {
11521158 "issue": {
11561162 "title": "Retry webhooks with exponential backoff",
11571163 "body": "Deliveries that fail are dropped today. Retry them with exponential backoff, up to six times, then mark the delivery failed.",
11581164 "labels": [],
1159− "checks": [
1160− "npm test"
1161− ],
11621165 "state": "open",
11631166 "reason": null,
11641167 "resolved_by": null,
12861289 "labels": [
12871290 "feature"
12881291 ],
1289− "checks": [
1290− "cargo test"
1292+ "done": [
1293+ "`greet` prints \"Hello, ana!\" when given \"ana\", and \"Hello, world!\" with no argument."
12911294 ],
12921295 "files": [
12931296 "src/greet.rs"
13011304 "labels": [
13021305 "feature"
13031306 ],
1304− "checks": [
1305− "cargo test"
1307+ "done": [
1308+ "The page at / shows the name given in ?name=, escaped."
13061309 ],
13071310 "files": [
13081311 "src/web.rs"
13481351 "labels": [
13491352 "feature"
13501353 ],
1351− "checks": [
1352− "cargo test"
1354+ "done": [
1355+ "`greet` prints \"Hello, ana!\" when given \"ana\", and \"Hello, world!\" with no argument."
13531356 ],
13541357 "files": [
13551358 "src/greet.rs"
13631366 "labels": [
13641367 "feature"
13651368 ],
1366− "checks": [
1367− "cargo test"
1369+ "done": [
1370+ "The page at / shows the name given in ?name=, escaped."
13681371 ],
13691372 "files": [
13701373 "src/web.rs"
15441547 "body": "Take a name from the first argument; fall back to world.",
15451548 "labels": [
15461549 "feature"
1547− ],
1548− "checks": [
1549− "cargo test"
15501550 ],
15511551 "state": "open",
15521552 "reason": null,
15861586 "created_at": "2026-10-01T18:33:10.420Z"
15871587 }
15881588 ],
1589− "checks": {
1590− "id": "chk_01m43sw8e2g6j0m4q8t2x6a0c4",
1591− "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
1592− "status": "passed",
1593− "results": [
1594− {
1595− "command": "cargo test",
1596− "passed": true,
1597− "exit_code": 0,
1598− "output": "test result: ok. 3 passed; 0 failed",
1599− "duration_ms": 18234
1600− }
1601− ],
1602− "error": null,
1603− "created_at": "2026-10-01T18:33:11.002Z",
1604− "finished_at": "2026-10-01T18:35:44.902Z"
1605− },
1589+ "checks": null,
16061590 "overlaps": [],
16071591 "behind": false,
16081592 "review_pending": false,
16231607 "declined": false
16241608 }
16251609 ],
1626− "statuses": []
1610+ "statuses": [
1611+ {
1612+ "context": "CI / pull_request",
1613+ "state": "success",
1614+ "description": "CI passed",
1615+ "target_url": "https://g1t.sh/syntaqx/hello/actions/runs/run_01m43sw8e2g6j0m4q8t2x6a0c4",
1616+ "updated_at": "2026-10-01T18:35:44.902Z"
1617+ }
1618+ ],
1619+ "required_checks": [
1620+ {
1621+ "name": "CI",
1622+ "state": "success",
1623+ "description": "CI passed",
1624+ "target_url": "https://g1t.sh/syntaqx/hello/actions/runs/run_01m43sw8e2g6j0m4q8t2x6a0c4"
1625+ }
1626+ ]
16271627 },
1628− "notes": "| Field | |\n| --- | --- |\n| `pull.files` | The files it changes, with lines added and removed. |\n| `checks` | The latest run of the acceptance checks against its head, with each command's output. |\n| `overlaps` | Other pull requests in progress that change the same files. |\n| `behind` | Whether the default branch has moved since it was made. |\n| `landing` | Whether it is being brought up to date to land. |\n| `lifecycle` | For a pull request the g1t agent is seeing through: its stage, such as `working`, `checking`, `reviewing` or `needs_you`. |\n| `comments` | Comments, reviews and events, with `path`, `line` and `verdict`. |\n| `messages` | Messages sent to the agent working on it. |\n| `statuses` | Commit statuses reported for its head, such as workflow runs. |\n| `mergeable` | Whether it merges cleanly into its target: `clean`, `conflicting`, `checking` (being worked out) or `unknown`. Worked out ahead of time whenever it or its target moves. |\n| `conflicts` | When `mergeable` is `conflicting`, the files that conflict. Resolve them by merging the target in, or have the g1t agent do it. |\n| `earlierChecks` | Earlier runs of the acceptance checks, newest first, without their output. |\n\nChecks are started by g1t, not through the API. They run when a pull request becomes ready for review and again when its head moves."
1628+ "notes": "| Field | |\n| --- | --- |\n| `pull.files` | The files it changes, with lines added and removed. |\n| `statuses` | Its checks: what each workflow run (or another tool, such as a deployment) reported on its head, as `pending`, `success`, `failure` or `error`, with a link to the run. |\n| `required_checks` | Each check the default branch requires (see [`update_repo_settings`](#update_repo_settings)), as it stands on the head: `success`, `failure`, `pending`, or `expected` when nothing has reported it yet. It merges only once all are `success`. |\n| `checks` | Set when the merge queue took it out, with why in `error`. Older pull requests may show a run of commands from their issue here, from before checks were workflows. |\n| `overlaps` | Other pull requests in progress that change the same files. |\n| `behind` | Whether the default branch has moved since it was made. |\n| `landing` | Whether it is being brought up to date to land. |\n| `lifecycle` | For a pull request the g1t agent is seeing through: its stage, such as `working`, `checking`, `reviewing` or `needs_you`. |\n| `comments` | Comments, reviews and events, with `path`, `line` and `verdict`. |\n| `messages` | Messages sent to the agent working on it. |\n\n| `mergeable` | Whether it merges cleanly into its target: `clean`, `conflicting`, `checking` (being worked out) or `unknown`. Worked out ahead of time whenever it or its target moves. |\n| `conflicts` | When `mergeable` is `conflicting`, the files that conflict. Resolve them by merging the target in, or have the g1t agent do it. |\n| `earlier_checks` | Earlier records like `checks`, newest first. |\n\nChecks are the repository's workflows: they run on `pull_request` events when it is opened, marked ready and pushed to. Read why one failed with [`get_workflow_run`](#get_workflow_run) and [`get_job_logs`](#get_job_logs)."
16291629 },
16301630 "get_pull_request_changes": {
16311631 "response": {
26832683 },
26842684 "response": true
26852685 },
2686+ "list_runners": {
2687+ "response": [
2688+ {
2689+ "id": "rnr_01kq2m8v4c7f0h3k6n9q2t5w8z",
2690+ "name": "build-01",
2691+ "workspace": "flagon-io",
2692+ "repo": null,
2693+ "group": "Default",
2694+ "labels": [
2695+ "self-hosted",
2696+ "linux",
2697+ "x64",
2698+ "gpu"
2699+ ],
2700+ "os": "linux",
2701+ "arch": "x64",
2702+ "version": "0.2.0",
2703+ "ephemeral": false,
2704+ "status": "busy",
2705+ "work": {
2706+ "kind": "workflow",
2707+ "id": "job_01kq2m9b1d4g7j0m3p6s9v2y5b",
2708+ "name": "test (linux, 24)",
2709+ "repo": "flagon-io/hello",
2710+ "run_id": "run_01kq2m9a8c1f4h7k0n3q6t9w2z",
2711+ "started_at": "2026-10-06T14:02:11.000Z"
2712+ },
2713+ "last_seen_at": "2026-10-06T14:05:40.512Z",
2714+ "created_at": "2026-10-02T09:30:00.000Z",
2715+ "created_by": null
2716+ },
2717+ {
2718+ "id": "rnr_01kq2m8v4c7f0h3k6n9q2t5w9a",
2719+ "name": "mac-mini",
2720+ "workspace": "flagon-io",
2721+ "repo": null,
2722+ "group": "Default",
2723+ "labels": [
2724+ "self-hosted",
2725+ "macos",
2726+ "arm64"
2727+ ],
2728+ "os": "macos",
2729+ "arch": "arm64",
2730+ "version": "0.2.0",
2731+ "ephemeral": false,
2732+ "status": "online",
2733+ "work": null,
2734+ "last_seen_at": "2026-10-06T14:05:40.512Z",
2735+ "created_at": "2026-10-02T09:30:00.000Z",
2736+ "created_by": null
2737+ }
2738+ ],
2739+ "notes": "A repository's list holds its own runners (`repo` set) and the workspace's that its runner group lets it use. A runner is `offline` when it has not polled for 90 seconds."
2740+ },
2741+ "list_runners_for_workspace": {
2742+ "params": {
2743+ "workspace": "flagon-io"
2744+ },
2745+ "response": [
2746+ {
2747+ "id": "rnr_01kq2m8v4c7f0h3k6n9q2t5w8z",
2748+ "name": "build-01",
2749+ "workspace": "flagon-io",
2750+ "repo": null,
2751+ "group": "Default",
2752+ "labels": [
2753+ "self-hosted",
2754+ "linux",
2755+ "x64",
2756+ "gpu"
2757+ ],
2758+ "os": "linux",
2759+ "arch": "x64",
2760+ "version": "0.2.0",
2761+ "ephemeral": false,
2762+ "status": "busy",
2763+ "work": {
2764+ "kind": "workflow",
2765+ "id": "job_01kq2m9b1d4g7j0m3p6s9v2y5b",
2766+ "name": "test (linux, 24)",
2767+ "repo": "flagon-io/hello",
2768+ "run_id": "run_01kq2m9a8c1f4h7k0n3q6t9w2z",
2769+ "started_at": "2026-10-06T14:02:11.000Z"
2770+ },
2771+ "last_seen_at": "2026-10-06T14:05:40.512Z",
2772+ "created_at": "2026-10-02T09:30:00.000Z",
2773+ "created_by": null
2774+ },
2775+ {
2776+ "id": "rnr_01kq2m8v4c7f0h3k6n9q2t5w9a",
2777+ "name": "mac-mini",
2778+ "workspace": "flagon-io",
2779+ "repo": null,
2780+ "group": "Default",
2781+ "labels": [
2782+ "self-hosted",
2783+ "macos",
2784+ "arm64"
2785+ ],
2786+ "os": "macos",
2787+ "arch": "arm64",
2788+ "version": "0.2.0",
2789+ "ephemeral": false,
2790+ "status": "online",
2791+ "work": null,
2792+ "last_seen_at": "2026-10-06T14:05:40.512Z",
2793+ "created_at": "2026-10-02T09:30:00.000Z",
2794+ "created_by": null
2795+ }
2796+ ]
2797+ },
2798+ "create_runner_registration_token": {
2799+ "response": {
2800+ "token": "g1trt_…",
2801+ "expires_at": "2026-10-06T15:04:00.000Z",
2802+ "workspace": "flagon-io",
2803+ "repo": "flagon-io/hello",
2804+ "group": null,
2805+ "url": "https://g1t.sh"
2806+ },
2807+ "notes": "Pass it to `g1t-runner register --url https://g1t.sh --token …` within the hour. See [self-hosted runners](/guides/self-hosted-runners/)."
2808+ },
2809+ "create_runner_registration_token_for_workspace": {
2810+ "params": {
2811+ "workspace": "flagon-io"
2812+ },
2813+ "request": {
2814+ "group": "Default"
2815+ },
2816+ "response": {
2817+ "token": "g1trt_…",
2818+ "expires_at": "2026-10-06T15:04:00.000Z",
2819+ "workspace": "flagon-io",
2820+ "repo": null,
2821+ "group": "Default",
2822+ "url": "https://g1t.sh"
2823+ }
2824+ },
2825+ "remove_runner": {
2826+ "params": {
2827+ "id": "rnr_01kq2m8v4c7f0h3k6n9q2t5w8z"
2828+ },
2829+ "response": true
2830+ },
2831+ "remove_runner_for_workspace": {
2832+ "params": {
2833+ "workspace": "flagon-io",
2834+ "id": "rnr_01kq2m8v4c7f0h3k6n9q2t5w8z"
2835+ },
2836+ "response": true
2837+ },
2838+ "get_runner_settings": {
2839+ "response": {
2840+ "agents_on_self_hosted": false,
2841+ "agent_labels": [
2842+ "self-hosted",
2843+ "linux"
2844+ ],
2845+ "fork_pull_requests": false,
2846+ "inherited": true
2847+ }
2848+ },
2849+ "get_runner_settings_for_workspace": {
2850+ "params": {
2851+ "workspace": "flagon-io"
2852+ },
2853+ "response": {
2854+ "agents_on_self_hosted": true,
2855+ "agent_labels": [
2856+ "self-hosted",
2857+ "linux"
2858+ ],
2859+ "fork_pull_requests": false,
2860+ "inherited": false
2861+ }
2862+ },
2863+ "update_runner_settings": {
2864+ "request": {
2865+ "inherit": true
2866+ },
2867+ "response": {
2868+ "agents_on_self_hosted": false,
2869+ "agent_labels": [
2870+ "self-hosted",
2871+ "linux"
2872+ ],
2873+ "fork_pull_requests": false,
2874+ "inherited": true
2875+ }
2876+ },
2877+ "update_runner_settings_for_workspace": {
2878+ "params": {
2879+ "workspace": "flagon-io"
2880+ },
2881+ "request": {
2882+ "agents_on_self_hosted": true,
2883+ "agent_labels": [
2884+ "linux"
2885+ ]
2886+ },
2887+ "response": {
2888+ "agents_on_self_hosted": true,
2889+ "agent_labels": [
2890+ "self-hosted",
2891+ "linux"
2892+ ],
2893+ "fork_pull_requests": false,
2894+ "inherited": false
2895+ },
2896+ "notes": "Agent work on your runners still uses g1t's model proxy, paid as before, unless the workspace has its own model provider."
2897+ },
2898+ "list_runner_groups": {
2899+ "params": {
2900+ "workspace": "flagon-io"
2901+ },
2902+ "response": [
2903+ {
2904+ "id": "rng_01kq2m7r2a5d8g1j4m7p0s3v6y",
2905+ "name": "Default",
2906+ "default": true,
2907+ "repositories": [],
2908+ "runners": 2,
2909+ "updated_at": "2026-10-02T09:30:00.000Z"
2910+ },
2911+ {
2912+ "id": "rng_01kq2m7r2a5d8g1j4m7p0s3v6z",
2913+ "name": "GPU",
2914+ "default": false,
2915+ "repositories": [
2916+ "hello",
2917+ "models"
2918+ ],
2919+ "runners": 1,
2920+ "updated_at": "2026-10-05T11:20:00.000Z"
2921+ }
2922+ ]
2923+ },
2924+ "create_runner_group": {
2925+ "params": {
2926+ "workspace": "flagon-io"
2927+ },
2928+ "request": {
2929+ "name": "GPU",
2930+ "repositories": [
2931+ "hello",
2932+ "models"
2933+ ]
2934+ },
2935+ "response": {
2936+ "id": "rng_01kq2m7r2a5d8g1j4m7p0s3v6z",
2937+ "name": "GPU",
2938+ "default": false,
2939+ "repositories": [
2940+ "hello",
2941+ "models"
2942+ ],
2943+ "runners": 1,
2944+ "updated_at": "2026-10-05T11:20:00.000Z"
2945+ }
2946+ },
2947+ "update_runner_group": {
2948+ "params": {
2949+ "workspace": "flagon-io",
2950+ "id": "rng_01kq2m7r2a5d8g1j4m7p0s3v6z"
2951+ },
2952+ "request": {
2953+ "repositories": []
2954+ },
2955+ "response": {
2956+ "id": "rng_01kq2m7r2a5d8g1j4m7p0s3v6z",
2957+ "name": "GPU",
2958+ "default": false,
2959+ "repositories": [],
2960+ "runners": 1,
2961+ "updated_at": "2026-10-05T11:20:00.000Z"
2962+ }
2963+ },
2964+ "delete_runner_group": {
2965+ "params": {
2966+ "workspace": "flagon-io",
2967+ "id": "rng_01kq2m7r2a5d8g1j4m7p0s3v6z"
2968+ },
2969+ "response": true
2970+ },
26862971 "list_webhooks": {
26872972 "response": [
26882973 {
+6−0
9999 Op::DeleteRepo => through::<repos::DeletedRepo>(op, sent),
100100 Op::ListDeletedRepos => through::<Vec<repos::DeletedRepo>>(op, sent),
101101 Op::GetRepoSettings | Op::UpdateRepoSettings => through::<work::RepoSettings>(op, sent),
102+ Op::ListCheckNames => through::<Vec<work::SeenCheck>>(op, sent),
102103 Op::GetMergeQueue => through::<work::QueueView>(op, sent),
103104 Op::ListIssues => through::<Vec<work::Issue>>(op, sent),
104105 Op::CreateIssue | Op::UpdateIssue | Op::CloseIssue | Op::ReopenIssue => {
119120 through::<actions::WorkflowRun>(op, sent)
120121 }
121122 Op::ListActionsSecrets | Op::ListActionsVariables => through::<Vec<actions::Setting>>(op, sent),
123+ Op::ListRunners => through::<Vec<g1t_contracts::runners::Runner>>(op, sent),
124+ Op::ListRunnerGroups => through::<Vec<g1t_contracts::runners::RunnerGroup>>(op, sent),
125+ Op::CreateRunnerGroup | Op::UpdateRunnerGroup => through::<g1t_contracts::runners::RunnerGroup>(op, sent),
126+ Op::GetRunnerSettings | Op::UpdateRunnerSettings => through::<g1t_contracts::runners::RunnerSettings>(op, sent),
127+ Op::CreateRunnerRegistrationToken => through::<g1t_contracts::runners::RegistrationToken>(op, sent),
122128 Op::ListWebhooks => through::<Vec<webhooks::Hook>>(op, sent),
123129 Op::ListIntegrations => through::<Vec<integrations::Connection>>(op, sent),
124130 Op::GetModelRoutes | Op::SetModelRoutes => through::<Vec<integrations::ModelRoute>>(op, sent),
+16−0
112112 Op::UpdateRepoSettings,
113113 &[],
114114 ),
115+ route("GET", "/repos/:owner/:name/check-names", Op::ListCheckNames, &[]),
115116 route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]),
116117 route(
117118 "POST",
488489 Op::DeleteActionsVariable,
489490 &[],
490491 ),
492+ // Self-hosted runners: a repository's own, or a workspace's.
493+ route("GET", "/repos/:owner/:name/actions/runners", Op::ListRunners, &[]),
494+ route("POST", "/repos/:owner/:name/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
495+ route("DELETE", "/repos/:owner/:name/actions/runners/:id", Op::RemoveRunner, &[]),
496+ route("GET", "/repos/:owner/:name/actions/runner-settings", Op::GetRunnerSettings, &[]),
497+ route("PATCH", "/repos/:owner/:name/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
498+ route("GET", "/workspaces/:workspace/actions/runners", Op::ListRunners, &[]),
499+ route("POST", "/workspaces/:workspace/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
500+ route("DELETE", "/workspaces/:workspace/actions/runners/:id", Op::RemoveRunner, &[]),
501+ route("GET", "/workspaces/:workspace/actions/runner-settings", Op::GetRunnerSettings, &[]),
502+ route("PATCH", "/workspaces/:workspace/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
503+ route("GET", "/workspaces/:workspace/actions/runner-groups", Op::ListRunnerGroups, &[]),
504+ route("POST", "/workspaces/:workspace/actions/runner-groups", Op::CreateRunnerGroup, &[]),
505+ route("PATCH", "/workspaces/:workspace/actions/runner-groups/:id", Op::UpdateRunnerGroup, &[]),
506+ route("DELETE", "/workspaces/:workspace/actions/runner-groups/:id", Op::DeleteRunnerGroup, &[]),
491507 route("POST", "/repos/:owner/:name/plans", Op::PlanWork, &[]),
492508 route("GET", "/repos/:owner/:name/plans/:plan", Op::GetPlan, &[]),
493509 route(
+84−0
1+//! What a self-hosted runner calls: registering with a registration token,
2+//! then polling for work, saying how work ended and removing itself, with
3+//! its own credential. Neither is a g1t access token, so these are served
4+//! before anything reads one, and no other endpoint accepts either.
5+//!
6+//! | Route | Body |
7+//! | --- | --- |
8+//! | `POST /runners/register` | `token`, `name`, `labels`, `os`, `arch`, `version`, `ephemeral`, `group`, `replace` |
9+//! | `POST /runners/{id}/poll` | `version`, `running`, `wait_ms` |
10+//! | `POST /runners/{id}/finished` | `id`, `exit_code`, `reason` |
11+//! | `POST /runners/{id}/remove` | nothing |
12+//!
13+//! The last three send `Authorization: Bearer g1tr_…`. See
14+//! `g1t_contracts::runners` for what each answers.
15+
16+use g1t_contracts::Outcome;
17+use g1t_kit::wire;
18+use serde_json::{Value, json};
19+use worker::{Request, Response, Result};
20+
21+use crate::operations::Services;
22+use crate::{fail, failure, json_body};
23+
24+/// What is passed through as given: an agent task's environment.
25+const AS_GIVEN: &[&str] = &["env"];
26+
27+fn credential(request: &Request) -> Result<String> {
28+ let header = request.headers().get("authorization")?.unwrap_or_default();
29+ Ok(match header.split_once(' ') {
30+ Some((scheme, token)) if scheme.eq_ignore_ascii_case("bearer") => token.trim().to_owned(),
31+ _ => String::new(),
32+ })
33+}
34+
35+async fn answer(services: &Services, method: &str, args: &Value) -> Result<Response> {
36+ let answered: Outcome<Value> = g1t_kit::call(&services.actions, method, args).await?;
37+ match answered {
38+ Outcome::Ok(value) => Response::from_json(&wire::snake_case_keeping(value, AS_GIVEN)),
39+ Outcome::Fail(refused) => failure(&refused),
40+ }
41+}
42+
43+/// Serves `path` if it is one of the runner's routes.
44+pub async fn handle(request: &mut Request, services: &Services, path: &str) -> Result<Option<Response>> {
45+ let Some(rest) = path.strip_prefix("/runners/") else { return Ok(None) };
46+ let body = json_body(request).await;
47+ let body = if body.is_object() { body } else { json!({}) };
48+ if rest == "register" {
49+ let args = json!({
50+ "token": body["token"].as_str().unwrap_or_default(),
51+ "name": body["name"].as_str().unwrap_or_default(),
52+ "labels": body["labels"].as_array().cloned().unwrap_or_default(),
53+ "os": body["os"].as_str().unwrap_or_default(),
54+ "arch": body["arch"].as_str().unwrap_or_default(),
55+ "version": body["version"].as_str().unwrap_or_default(),
56+ "ephemeral": body["ephemeral"].as_bool().unwrap_or(false),
57+ "group": body["group"].as_str(),
58+ "replace": body["replace"].as_bool().unwrap_or(false),
59+ });
60+ return Ok(Some(answer(services, "runner_register", &args).await?));
61+ }
62+ let Some((runner, action)) = rest.split_once('/') else {
63+ return Ok(Some(fail(g1t_contracts::FailureCode::NotFound, "No such endpoint.")?));
64+ };
65+ let auth = json!({ "runner": runner, "credential": credential(request)? });
66+ let mut args = auth;
67+ let method = match action {
68+ "poll" => {
69+ args["version"] = json!(body["version"].as_str().unwrap_or_default());
70+ args["running"] = json!(body["running"].as_array().cloned().unwrap_or_default());
71+ args["wait_ms"] = json!(body["wait_ms"].as_u64().unwrap_or(0));
72+ "runner_poll"
73+ }
74+ "finished" => {
75+ args["id"] = json!(body["id"].as_str().unwrap_or_default());
76+ args["exit_code"] = json!(body["exit_code"].as_i64().unwrap_or(1));
77+ args["reason"] = body["reason"].clone();
78+ "runner_finished"
79+ }
80+ "remove" => "runner_remove_self",
81+ _ => return Ok(Some(fail(g1t_contracts::FailureCode::NotFound, "No such endpoint.")?)),
82+ };
83+ Ok(Some(answer(services, method, &args).await?))
84+}
+19−6
6464 a("get", Op::GetRepo, "One repository"),
6565 a("create", Op::CreateRepo, "Create one, empty or copied from a public git URL"),
6666 a("update", Op::UpdateRepo, "Change description, website, topics, default branch, protection"),
67− a("get_settings", Op::GetRepoSettings, "How pull requests merge"),
68− a("update_settings", Op::UpdateRepoSettings, "Change how pull requests merge"),
67+ a("get_settings", Op::GetRepoSettings, "Branch protection: required checks, approvals, how pull requests merge"),
68+ a("update_settings", Op::UpdateRepoSettings, "Change branch protection and how pull requests merge"),
69+ a("check_names", Op::ListCheckNames, "Check names reported lately, to require on the default branch"),
6970 a("list_labels", Op::ListLabels, "Labels in use"),
7071 a("list_events", Op::ListEvents, "Timeline: pushes, issues, pull requests, comments"),
7172 a("rename_branch", Op::RenameBranch, "Rename a branch"),
8788 default_action: None,
8889 actions: &[
8990 a("list", Op::ListIssues, "Issues on a repository, newest first"),
90− a("get", Op::GetIssue, "One issue with comments, checks and its pull requests"),
91+ a("get", Op::GetIssue, "One issue with comments and its pull requests"),
9192 a("create", Op::CreateIssue, "Open an issue"),
9293 a("update", Op::UpdateIssue, "Change title, body, labels or assignees"),
9394 a("close", Op::CloseIssue, "Close it without a pull request"),
103104 default_action: None,
104105 actions: &[
105106 a("list", Op::ListPullRequests, "Pull requests on a repository, newest first"),
106− a("get", Op::GetPullRequest, "Status, checks, reviews, overlaps and whether it is behind"),
107+ a("get", Op::GetPullRequest, "Status, checks and required checks, reviews, overlaps, whether it is behind"),
107108 a("changes", Op::GetPullRequestChanges, "Files and line-by-line diff"),
108109 a("create", Op::CreatePullRequest, "Start a draft with its own fork to push to, or open one from a pushed branch"),
109110 a("record_session", Op::RecordSession, "Append prompt, reasoning and tool entries to its session"),
131132 Tool {
132133 name: "plan",
133134 title: "Plans",
134− description: "Turn an outcome into issues: an agent proposes them with checks and dependencies; nothing opens until you apply the plan.",
135+ description: "Turn an outcome into issues: an agent proposes them with what done means and their dependencies; nothing opens until you apply the plan.",
135136 default_action: None,
136137 actions: &[
137138 a("create", Op::PlanWork, "Ask an agent for a plan; read it with get until ready"),
152153 Tool {
153154 name: "workflow",
154155 title: "Workflows",
155− description: "GitHub Actions workflows from .g1t/workflows: their runs, jobs and logs, and running, cancelling or rerunning them.",
156+ description: "GitHub Actions workflows from .g1t/workflows: their runs, jobs and logs, and running, cancelling or rerunning them. Also the self-hosted runners they run on: a workspace's (`workspace`) or a repository's own (`repo`), their groups, and where agent work runs.",
156157 default_action: None,
157158 actions: &[
158159 a("list", Op::ListWorkflows, "Workflows on the default branch"),
163164 a("cancel", Op::CancelWorkflowRun, "Cancel a run"),
164165 a("rerun", Op::RerunWorkflowRun, "Run a finished run again"),
165166 a("update", Op::UpdateWorkflow, "Turn a workflow on or off"),
167+ a("list_runners", Op::ListRunners, "Self-hosted runners, with status, labels and what each is doing"),
168+ a("create_runner_token", Op::CreateRunnerRegistrationToken, "A one-hour token for g1t-runner register"),
169+ a("remove_runner", Op::RemoveRunner, "Remove a self-hosted runner"),
170+ a("list_runner_groups", Op::ListRunnerGroups, "A workspace's runner groups"),
171+ a("create_runner_group", Op::CreateRunnerGroup, "Make a group, for some repositories"),
172+ a("update_runner_group", Op::UpdateRunnerGroup, "Rename a group or change its repositories"),
173+ a("delete_runner_group", Op::DeleteRunnerGroup, "Delete a group; its runners join the default"),
174+ a("get_runner_settings", Op::GetRunnerSettings, "Where agent work runs; whether forks may use runners"),
175+ a("update_runner_settings", Op::UpdateRunnerSettings, "Change them"),
166176 ],
167177 },
168178 Tool {
272282 | Op::SetModelRoutes
273283 | Op::SetBasePermission
274284 | Op::MergePullRequest
285+ | Op::RemoveRunner
286+ | Op::DeleteRunnerGroup
287+ | Op::UpdateRunnerSettings
275288 )
276289 }
277290
+6−2
33 "name": "g1t-api",
44 "account_id": "1e6f2cffa3f445920836e8ebe446bb58",
55 "compatibility_date": "2026-09-26",
6+ "placement": { "mode": "off" },
67 "main": "build/index.js",
78 "build": { "command": "node ../../scripts/build-rust-worker.mjs" },
89 "workers_dev": false,
2829 { "binding": "CONTEXT", "service": "g1t-context" },
2930 { "binding": "SEARCH", "service": "g1t-search" }
3031 ],
31− // GitHub Actions artifacts and cache, in chunks, with KV's own expiry.
32− // (Moves to R2 once R2 is enabled on the account.)
32+ // GitHub Actions artifacts, in chunks, with KV's own expiry (and cache
33+ // entries saved before the cache moved to R2, until they expire).
3334 "kv_namespaces": [{ "binding": "BLOBS", "id": "16a4232cb746418db53782aa068be693" }],
35+ // actions/cache entries, up to 2 GB each, uploaded in parts. The actions
36+ // service lists them and decides what is kept (services/actions/src/cache.rs).
37+ "r2_buckets": [{ "binding": "ACTIONS_CACHE", "bucket_name": "g1t-actions-cache" }],
3438 "observability": { "enabled": true }
3539 }
+6−0
9292 { label: 'Model providers', slug: 'guides/models' },
9393 { label: 'Webhooks', slug: 'guides/webhooks' },
9494 { label: 'GitHub Actions', slug: 'guides/actions' },
95+ { label: 'Self-hosted runners', slug: 'guides/self-hosted-runners' },
9596 ],
9697 },
9798 {
123124 items: [
124125 { label: 'API overview', slug: 'reference/api' },
125126 { label: 'MCP tools', slug: 'reference/mcp' },
127+ { label: "What g1t can't do yet", slug: 'reference/limitations' },
126128 { label: 'Try it in the explorer', link: '/api/reference/', attrs: { target: '_self' } },
127129 { label: 'OpenAPI document', link: 'https://api.g1t.sh/openapi.json' },
128130 { label: 'llms.txt', link: 'https://g1t.sh/llms.txt' },
129131 ],
130132 },
133+ {
134+ label: 'About',
135+ items: [{ label: 'An open letter to Cloudflare', slug: 'about/open-letter-to-cloudflare' }],
136+ },
131137 ...apiGroups,
132138 ],
133139 }),
+147−0
1+---
2+title: An open letter to Cloudflare
3+description: From the team at Flagon, Inc. building g1t, a git platform that runs entirely on Cloudflare. What works, where we hit walls, and what we'd ask for.
4+---
5+
6+Dear Cloudflare,
7+
8+We're the small team at Flagon, Inc. building g1t, a git platform where
9+people and coding agents work in the same issues, pull requests and merge
10+queue. Every part of it runs on you: about twenty Workers, a D1 database per
11+service, Artifacts for every repository and every pull request, Containers
12+for agents and CI, R2, KV, Queues, and Cloudflare for SaaS for our customers'
13+domains. We have no servers.
14+
15+This is a thank-you, and a list of what would help us most next.
16+
17+## Why we built on you
18+
19+A git platform is usually a fleet: storage nodes, a job system, a database
20+cluster, a CDN in front, and people on call for all of it. We wanted to run
21+one for thousands of teams with a handful of people. You offered a global
22+platform where the unit of work is a request, the unit of storage is a
23+Durable Object, and nothing costs money while nobody is using it.
24+
25+Artifacts is the reason g1t exists in this shape. One Durable Object per
26+repository is the right isolation unit: a busy repository doesn't slow its
27+neighbours, and there is nothing to shard by hand. It speaks real git, so
28+stock clients cloned, fetched and pushed through our proxy from the first
29+day. `fork()` is a single call, and per-pull-request isolation for agents
30+fell out of it almost for free. Scoped tokens that expire on their own let
31+us hand a sandbox a credential that dies with it. The read binding powers
32+every page we render, blame, mergeability and search, without a git client
33+anywhere.
34+
35+The rest of the platform held up too. Rust compiled to WebAssembly runs our
36+services. D1's read replication is free and good. Containers gave us
37+sandboxes in three sizes. With a cached credential and ref listing, a
38+`git fetch` with nothing new answers in under half a second, and most of
39+our pages answer in under 250 ms. We went from an empty repository to a
40+launch on this stack, and most of it worked the first time.
41+
42+## Where we hit walls
43+
44+Running a real platform for many teams found the edges. None of these
45+stopped us. Each one costs us code, latency or certainty, and each one will
46+cost the next team building on you the same.
47+
48+**What a billable operation is.** Artifacts pricing names "repo operations,
49+such as create, push, pull, and clone", and the metrics list a different set
50+of event names. Neither says whether binding reads, token mints or ref
51+listings count. We price from cost, so this decides what our customers pay.
52+Depending on the answer, our model for a few thousand workspaces lands
53+anywhere between about $1.8k and $31k a month. Today we count every clone,
54+fetch and push ourselves, and hope it matches.
55+
56+**What a fork stores.** Forks are the natural primitive for a pull request,
57+and agents open pull requests by the thousand. We can't find whether a fork
58+shares objects with its source or copies them. If it copies, an agent-heavy
59+account reaches the 1 TB account limit in days, and at that point every push
60+in the account fails, for every customer at once. We keep forks for now,
61+and are measuring it ourselves.
62+
63+**A write path and a pre-receive hook.** The binding reads, but it can't
64+list refs, move them, or write objects. So to land a pull request we speak
65+git's wire protocol to our own storage from inside a Worker, buffering packs
66+in an isolate with 128 MB to share. With no hook before refs move, branch
67+protection and secret scanning only hold for pushes through our proxy, which
68+parses every pack in WebAssembly before forwarding it. We wrote a second
69+implementation of git's pack format to get there.
70+
71+**Ref-change events and the cost of a credential.** Push events need one
72+subscription per repository, which doesn't scale to tens of thousands of
73+repositories and forks. We record ref changes ourselves, and a test scans
74+our own source to make sure every code path that moves a ref says so. Every
75+git credential takes three binding calls and about 0.8 s to mint, so we
76+cache sealed tokens across isolates in KV.
77+
78+**Placement that follows data.** Smart Placement once ran our site in
79+Amsterdam for a visitor in Denver, while every D1 primary we have is in
80+western North America. Every query crossed the Atlantic, and our Explore
81+page took 0.85 s instead of 0.17 s. We turned placement off everywhere and
82+measure each Worker by hand.
83+
84+**D1 sessions across service bindings.** Read replicas need a bookmark to
85+give read-your-writes. Our site calls seven services, each with its own
86+database, so we built a header protocol to carry bookmarks through service
87+bindings into a cookie and back.
88+
89+**Containers that build images and keep disks.** We found no supported way
90+to run Docker or BuildKit in a Container, so our own CI can't rebuild our
91+sandbox image; that waits for a machine outside. Container disk is
92+ephemeral, so the self-hostable git store we'd like as a warm fallback has
93+to live off Cloudflare.
94+
95+**Inbound TCP for SSH.** Git users expect `git@host:owner/repo`. Workers
96+take no inbound TCP, so g1t is HTTPS only. We've applied for the beta and
97+are waiting.
98+
99+## What we built in the meantime
100+
101+A per-workspace operation counter that is our best guess at your invoice. A
102+fork sweep we can switch on once we know what forks cost. A smart HTTP
103+client inside a Worker for landing, catch-up, mirrors and imports. Our own
104+push policy in front of Artifacts. A versioned ref cache with a test that
105+guards it. Two layers of credential caching. A bookmark protocol for D1.
106+A probe that deploys throwaway Workers to measure placement, and a
107+`Server-Timing` header on every response so we see the next regression.
108+Image builds on a laptop.
109+
110+All of it works. Most of it is code we'd happily delete.
111+
112+## What we're asking for
113+
114+1. A published definition of a billable Artifacts operation, with
115+ per-repository metrics that use the same names as the invoice.
116+2. Documented fork storage, an expiry on `fork()`, a repository's stored
117+ bytes in `info()`, and a warning before the account storage limit, with
118+ failures per repository rather than account-wide.
119+3. Ref listing, atomic compare-and-swap ref updates and streaming pack
120+ writes in the binding.
121+4. A pre-receive hook that a Worker answers.
122+5. Account-level ref-change events to a Queue, a read-after-write guarantee
123+ for refs, and git forwarding authenticated by the binding, with no token
124+ to mint.
125+6. Placement that accounts for D1 primaries and service bindings, and D1 as
126+ a placement target.
127+7. D1 sessions that travel across service bindings.
128+8. Image builds and persistent volumes for Containers.
129+9. Inbound TCP, so git can run over SSH.
130+10. A support path during the beta, snapshot restore and export for
131+ repositories, and a date for general availability with an SLA.
132+
133+## Let's work on it together
134+
135+We chose you on purpose, and we'd choose you again. A small team running a
136+global git platform with no servers is the promise of what you've built,
137+and g1t shows that it mostly holds. We'd like to help close the
138+rest of the gap: traces, test repositories, early builds to try, or a call
139+with the teams involved. Whatever is useful.
140+
141+You can reach us through [g1t.sh](https://g1t.sh/support). Our code lives
142+at [g1t.sh/flagon-io/g1t](https://g1t.sh/flagon-io/g1t), on the platform
143+it describes.
144+
145+With thanks,
146+
147+The team at Flagon, Inc.
+25−31
55
66 g1t is where people and agents ship software together. You hand g1t an
77 outcome, and agents converge it onto `main`: each change is made in a pull
8−request of its own, checked in a clean sandbox, reviewed, revised and merged
8+request of its own, checked by your workflows, reviewed, revised and merged
99 under your repository's rules. People work alongside the agents in the same
1010 repositories, issues, pull requests and reviews, and every change can deploy
1111 to the edge.
2222 | **Pull request** | A proposed change, in its own fork or on a branch. Usually made for an issue. |
2323 | **Session** | The record of how a pull request was made: prompts, reasoning, tool calls. |
2424
25−The part that is different from other forges: one issue routinely has
25+What g1t adds: one issue routinely has
2626 several pull requests, each from a different agent, and g1t keeps track of
2727 which one was merged.
2828
3535 An issue has:
3636
3737 - a **title** and a **description** in Markdown. An agent given the issue
38− works from this text;
38+ works from this text. It can say what done means in plain words,
39+ under a `## Definition of done` heading if you like: context for the
40+ agent and its reviewers, not something a merge waits on;
3941 - **labels**, which say what kind of issue it is;
40−- **acceptance checks**: commands a pull request should make pass, which
41− g1t [runs itself](#acceptance-checks);
4242 - **comments**;
4343 - a **state**: open or closed. A closed issue records why: `completed` or
4444 `not_planned`.
119119 that the issue should stay open. The pull request merges, and the issue and
120120 the other pull requests are left as they are.
121121
122−## Acceptance checks
122+## Checks
123123
124−An issue can list **acceptance checks**: commands, such as `cargo test`,
125−that a pull request for it should make pass.
124+A pull request's checks are what the repository's
125+[workflows](/guides/actions/) report on its head commit. Every workflow
126+that runs on `pull_request` runs on every pull request, whoever opened it,
127+a person or an agent, and reports a check named after the workflow, such as
128+`CI`.
126129
127−When a pull request for that issue is ready for review, g1t runs the checks
128−itself. It starts a sandbox that holds nothing but the pull request's head
129−commit, runs each command there, and records whether it passed and what it
130−printed. Pushing to the pull request runs them again.
130+The default branch decides which checks a merge needs: its
131+[required status checks](/guides/pull-requests/#required-status-checks).
132+A pull request merges only once each of them has passed on its head. One
133+that failed, is still running or has not reported yet holds the merge,
134+unless the repository allows bypassing them and someone who can merge
135+chooses to. Checks that are not required are shown, and never hold a merge.
131136
132−- The sandbox is clean. No agent has worked in it, so a pass says something
133− about the code and not about what was left lying around.
134−- Only that sandbox can report the result. An agent cannot mark its own work
135− as passing.
136−- Each pull request for an issue is checked the same way, which makes
137− several of them comparable at a glance.
138−
139−A pull request whose checks have not passed cannot be merged, unless
140−someone who can merge chooses to merge anyway.
141−
142−Checks run in g1t's sandboxes, so they need a workspace on the
143−[g1t plan](/guides/usage-and-billing/#the-g1t-plan), or one with
144−[trial credit](/guides/usage-and-billing/#the-trial) left. On a public
145−repository, [g1t's open-source pool](/guides/usage-and-billing/#the-open-source-pool)
146−runs them too, after a card check.
137+The same rules hold for people and agents. A g1t agent's pull request is
138+checked by the same workflows as yours, and an agent cannot mark its own
139+work as passing: only the workflow runs report.
147140
148141 ## Review
149142
186179
187180 Someone with the [Write role](/guides/access-and-roles/) or higher on the
188181 repository merges a pull request once it is
189−marked ready and its checks have passed. Merging moves `main` to the pull
182+marked ready and its [required checks](/guides/pull-requests/#required-status-checks)
183+have passed. Merging moves `main` to the pull
190184 request's head commit, or, in a repository that merges through
191185 [the merge queue](/guides/merge-queue/), adds it to the queue.
192186
200194 into the pull request in a sandbox: if the merge is clean, it is pushed as it
201195 is; if it conflicts, the agent is given the conflicted files and what the
202196 pull request is for, resolves them, and pushes the result, and the session
203−records what was done. Either way the checks run again on the result
197+records what was done. Either way the workflows run again on the result
204198 ([how catching up works](/guides/pull-requests/#catching-up)). You can also do it by
205199 hand: pull `main` into the fork or the branch, resolve, and push. `main` never loses a commit this way, however many
206200 pull requests are in flight.
210204 Merging one pull request at a time keeps every merge clean as text, but two
211205 changes can merge without a conflict and still break each other. A
212206 repository that turns on **Merge through a queue** tests each pull request
213−together with the ones ahead of it, along with the checks of every issue
214−already completed, and `main` only moves to a state whose checks passed.
207+together with the ones ahead of it, and `main` only moves to a state whose
208+required checks passed.
215209 See [merge queue](/guides/merge-queue/).
216210
217211 ## Sessions and why-blame
+119−14
4444 | `secrets.*`, `vars.*`, `secrets.GITHUB_TOKEN` | The same. `secrets.G1T_TOKEN` is the workspace's own token for the run; `GITHUB_TOKEN` is its alias. |
4545 | `environment:` on a job | The job reads each key's row for that environment, as GitHub's environment secrets work. |
4646 | `actions/upload-artifact`, `actions/download-artifact` | Kept with the run for 14 days, passed between its jobs, and downloadable from the run's page. Up to 60 MB each. |
47−| `actions/cache`, `actions/cache/restore`, `actions/cache/save` | Kept per repository for 7 days, found by `key` or the newest under a `restore-keys` prefix. Up to 60 MB each. |
47+| `actions/cache`, `actions/cache/restore`, `actions/cache/save` | Kept per repository, found by `key` or the newest under a `restore-keys` prefix. `path` takes globs and `!` exclusions. Up to 2 GB each; see [the cache](#the-cache). |
4848
4949 The **Actions** page of a workflow says, under *How this runs on g1t*,
5050 anything in it that runs differently.
5151
5252 ### Not yet
5353
54−- **Windows and macOS runners.** Jobs run on Linux; a job with
55− `runs-on: windows-latest` or `macos-latest` fails, and says so.
54+- **Windows and macOS on g1t's machines.** g1t's own runners are Linux; a
55+ job with `runs-on: windows-latest` or `macos-latest` fails, and says so.
56+ [Self-hosted runners](/guides/self-hosted-runners/) of any OS run them:
57+ `runs-on: [self-hosted, windows]`.
5658 - **Docker** container actions, `services:` containers and `container:`.
5759 - **Reusable workflows from other repositories** (`uses: owner/repo/.github/workflows/x.yml@v1`); ones in the same repository work.
5860 - **The toolkit's own cache.** Actions that cache through GitHub's service
7375 `actions/setup-node` and `actions/setup-python` install other versions as
7476 they do on GitHub.
7577
78+### Machine sizes
79+
80+A job runs on the standard machine unless its `runs-on` names a larger
81+one:
82+
83+| `runs-on` | vCPUs | Memory | Disk |
84+| --- | --- | --- | --- |
85+| `ubuntu-latest`, or any other Linux label | 0.5 | 4 GiB | 8 GB |
86+| `g1t-2core` | 2 | 8 GiB | 16 GB |
87+| `g1t-4core` | 4 | 12 GiB | 20 GB |
88+
89+```yaml
90+jobs:
91+ build:
92+ runs-on: g1t-4core
93+```
94+
95+The label can come from the matrix or the run's inputs
96+(`runs-on: ${{ matrix.big && 'g1t-4core' || 'ubuntu-latest' }}`). A
97+larger machine costs what it costs g1t, plus the same margin as all
98+sandbox time: see [usage and billing](/guides/usage-and-billing/#workflow-jobs-on-larger-machines).
99+Builds that compile, such as Rust or a large TypeScript project, finish
100+several times faster on one.
101+
76102 A job runs for at most 60 minutes, whatever its `timeout-minutes`, and
77103 for less if the workspace's plan caps runs lower (a new workspace's first
78104 month, or the trial). A job stopped at its time cap fails saying so.
92118 allowed domains under **Settings → Guardrails**; a project whose guardrails
93119 turn the network restriction off runs its jobs with an open network.
94120
121+A host only workflows should reach, such as the API a deploy uploads to,
122+goes in **Workflow-only domains** instead, limited to the workflows and
123+environments that need it: `api.cloudflare.com | deploy.yml | production`
124+lets only `deploy.yml`'s jobs with `environment: production` reach it.
125+Agents never reach those hosts, and neither do runs of pull requests from
126+forks. See [workflow-only domains](/guides/guardrails/#workflow-only-domains).
127+
95128 g1t does not run cryptocurrency miners: a step that names one (`xmrig`,
96129 a `stratum+tcp://` pool, `--donate-level`) is not run, and a job that
97130 looks like it is mining is stopped. See
98131 [abuse and mining](/guides/guardrails/#abuse-and-mining).
99132
133+## The cache
134+
135+`actions/cache` keeps what a job saves for the repository's later jobs:
136+
137+| | |
138+| --- | --- |
139+| One entry | Up to 2 GB, compressed. A larger one is not saved, and the job goes on. |
140+| A repository's entries | Up to 10 GB together. Saving past it removes the entries restored longest ago. |
141+| How long | Until it has not been restored for 7 days, and at most 28 days after it was saved. |
142+| Keys | Written once: saving under a key that exists does nothing. A restore finds its `key` exactly, else the newest entry whose key starts with one of its `restore-keys`. |
143+| `path` | Files and folders; globs, `**` included; `~/` is the home folder; a line starting with `!` leaves matching paths out. |
144+| Compression | zstd. |
145+
146+```yaml
147+- uses: actions/cache@v4
148+ with:
149+ path: |
150+ ~/.cargo/registry/cache
151+ target/release
152+ !target/**/incremental
153+ key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
154+ restore-keys: cargo-${{ runner.os }}-
155+```
156+
157+Each restore and save says on the job's log how large the entry was and
158+how long it took. A workspace on the plan pays for what its caches hold
159+(`Actions cache storage` on its statement), at R2's price plus the margin;
160+see [usage and billing](/guides/usage-and-billing/#actions-cache).
161+
100162 ## Runs and logs
101163
102164 Open a repository's **Actions** page, in its sidebar. Pick a workflow to
115177 marks it ready, which on g1t is when it first has code. Each head runs
116178 each workflow once.
117179
118−A run on a pull request's latest commit is a check on it:
180+## Checks
119181
120−- While a workflow runs, the pull request waits for it before merging.
121−- When one fails, merging is refused, as for failed acceptance checks.
122− Where the repository allows ignoring checks, anyone who can merge can
123− merge anyway.
124−- In a repository that merges through the [merge queue](/guides/merge-queue/),
182+A pull request's checks are its workflows. Each workflow that runs on
183+`pull_request` runs on every pull request's head, whoever opened it, a
184+person or an agent, and its runs report a check named after the workflow:
185+a workflow with `name: CI` reports `CI`, with the status context
186+`CI / pull_request` (the workflow's name and the event).
187+
188+- **Which checks a merge needs** is up to the default branch's
189+ [required status checks](/guides/pull-requests/#required-status-checks),
190+ under **Settings → Branches and merging**. A required check that failed,
191+ is still running or has not reported holds the merge. Checks that are not
192+ required are shown on the pull request and never hold it.
193+- **In a repository that merges through the [merge queue](/guides/merge-queue/)**,
125194 workflows with `on: merge_group` run on each combined state the queue
126− builds, as on GitHub, and the state lands only if they pass.
127−- A pull request a **g1t agent** is working on goes back to the agent
128− when a workflow fails. The agent reads the run and its logs with the
129− same tools you have, fixes the cause, and pushes; the workflows run
130− again.
195+ builds, on the branch `g1t-queue/<entry>`, and the state lands only if
196+ they and every required check pass on it. A workflow behind a required
197+ check needs `merge_group` in its `on:`.
198+- **A pull request a g1t agent is working on** goes back to the agent when
199+ a check fails, with the end of each failed job's log. The agent reads the
200+ run and its logs with the same tools you have, fixes the cause, and
201+ pushes; the workflows run again. See
202+ [seeing it through](/guides/g1t-agents/#seeing-it-through).
203+
204+```yaml
205+name: CI
206+
207+on:
208+ pull_request:
209+ push:
210+ branches: [main]
211+ merge_group:
212+```
213+
214+### Add CI
215+
216+A repository with no workflows has nothing that proves a change works, for
217+people or for agents. Its pull requests, its **Branches and merging**
218+settings and its **Actions** page say **This repository has no checks**,
219+with an **Add CI** button. Anyone who can push to the repository can use it:
220+
221+1. Choose **Add CI**. g1t looks at the files at the repository's root and
222+ writes a starter workflow with a job for each stack it finds, up to
223+ three: Node (npm, pnpm, Yarn or Bun), Rust, Go, Python (pip or uv), Ruby,
224+ Java (Maven or Gradle), .NET, or Make. Each job installs, lints where
225+ the project says how, builds and tests. When it finds none, the job is a
226+ placeholder that fails until you replace its last step with your own
227+ commands.
228+2. The workflow is committed as `.g1t/workflows/ci.yml` on a new branch,
229+ `add-ci`, and opened as a pull request, by you. It is named `CI` and runs
230+ on `pull_request`, on `push` to the default branch, and on `merge_group`.
231+3. Change it on the pull request if the steps are not how your project
232+ builds, and merge it.
233+4. Once it has run, `CI` is offered under **Required status checks**.
234+ Require it, so that nothing merges into the default branch unless it
235+ passes.
131236
132237 ## Secrets and variables
133238
+2−2
3232 | `update` | Merges in the branch its pull request will land on. |
3333 | `plan` | Turns an outcome into a plan of issues. |
3434
35−Sandboxes that run commands rather than a model show too, as `checks` and
36−`queue`, so the list is everything g1t is running for the project.
35+Sandboxes that run commands rather than a model show too, such as `queue`
36+for the merge queue's builds, so the list is everything g1t is running for the project.
3737
3838 Each run records:
3939
+4−1
323323 | Memory & search | `memory:read`, `memory:write` |
324324 | Account | `account:read`, `account:write` |
325325 | Workspace | `workspace:read`, `access:read`, `webhooks:read`, `secrets:read` |
326−| Dangerous | `repo:admin`, `workspace:admin`, `access:admin`, `webhooks:admin`, `secrets:admin` |
326+| Runners | `runners:read` |
327+| Dangerous | `repo:admin`, `workspace:admin`, `access:admin`, `webhooks:admin`, `secrets:admin`, `runners:admin` |
327328
328329 Ticking a higher level ticks the lower ones of its resource and greys
329330 them out: tick `issues:write` and `issues:read` is ticked too. Untick
355356 | `webhooks:admin` | Create, change and delete webhooks |
356357 | `secrets:read` | List secrets (never their values) and read variables |
357358 | `secrets:admin` | Set and delete secrets and variables |
359+| `runners:read` | See [self-hosted runners](/guides/self-hosted-runners/), their groups and where agents run. Not in the Agent preset. |
360+| `runners:admin` | Register and remove self-hosted runners, change their groups and settings |
358361
359362 Every operation of the API and the MCP server needs exactly one of these,
360363 except `whoami` (`GET /user`), which any token may use. Each endpoint's page
+8−6
8989
9090 ## How an agent works on an issue
9191
92−1. `issue` with `get`, to read the description and acceptance checks, and
93− to see which pull requests already exist for it.
92+1. `issue` with `get`, to read the description, including what done means
93+ if it says, and to see which pull requests already exist for it.
9494 2. `memory` with `recall`, to read what the project remembers: how to
9595 build, conventions and traps.
9696 3. `pull_request` with `create` and the issue's number. This opens a draft
107107 { "name": "pull_request", "arguments": { "action": "create", "repo": "flagon-io/hello", "issue": 42, "agent": "claude-code" } }
108108 ```
109109
110−When the pull request is ready, g1t runs the issue's acceptance checks
111−against it in a clean sandbox. `pull_request` with `get` returns each command's
112−result and output, so an agent whose checks failed can read why, push a fix,
113−and have them run again.
110+Every push runs the repository's [workflows](/guides/actions/#checks) on
111+the pull request, as for anyone's. `pull_request` with `get` returns its
112+`statuses` and `required_checks`, the checks the default branch requires
113+before it merges. When one fails, `workflow` with `get_run` and `job_logs`
114+says why; push a fix and the workflows run again. Run the same tests and
115+linters the workflows run before you push, and you will rarely need to.
114116
115117 If merging reports that `main` has moved, pull `main` from the repository
116118 into the fork and push. The pull request can then be merged.
+2−2
164164 | **No open secret findings** | [Security](/guides/security/) has no open secret findings for it |
165165
166166 A failing rule has **Fix with an agent**: g1t opens an issue saying what to
167−do, with an acceptance check where one can be written (such as
168−`test -f AGENTS.md`), and puts g1t's agent on it.
167+do, with a definition of done where one can be written (such as
168+"`test -f AGENTS.md` passes."), and puts g1t's agent on it.
169169
170170 ## Agents start with context
171171
+74−32
2525
2626 1. On Mission control, choose **Put an agent on it**.
2727 2. Pick the project, give a title, and say what you want done in plain
28− words. Add acceptance checks, one command per line, if you know them.
28+ words, with what done means if you know it.
2929 3. Choose **Put an agent on it**.
3030
3131 You land on the new issue with the agent already at work on its pull
4949 curl -X POST https://api.g1t.sh/repos/<workspace>/<repo>/issues/delegate \
5050 -H "Authorization: Bearer $G1T_TOKEN" \
5151 -H "Content-Type: application/json" \
52− -d '{"title": "Retry webhooks with exponential backoff", "body": "Deliveries that fail are dropped today. Retry them up to six times.", "checks": ["npm test"]}'
52+ -d '{"title": "Retry webhooks with exponential backoff", "body": "Deliveries that fail are dropped today. Retry them up to six times."}'
5353 ```
5454
5555 The answer holds the `issue`, the `pull` request the agent opened (or
5858 server it is the `agent` tool's `delegate` action. See
5959 [put an agent on it](/reference/api/issues/delegate/).
6060
61+The `checks` field this call and `create_issue` used to take is
62+deprecated. It is still accepted: its commands are added to the issue's
63+body under `## Definition of done`, one line each (`` - `npm test` passes. ``),
64+and the answer carries a `deprecation` string saying so. What has to pass
65+before the pull request merges is the default branch's
66+[required status checks](/guides/pull-requests/#required-status-checks).
67+
6168 ## Assigning agents
6269
6370 One issue:
166173 Making the change is the first step. g1t takes the rest itself, and you
167174 get the pull request back ready to merge:
168175
169−1. **Checks.** The issue's
170− [acceptance checks](/concepts/overview/#acceptance-checks) run against
171− the change in a separate, clean sandbox. The agent has no say in the
172− result.
176+1. **Checks.** Every push the agent makes runs the repository's
177+ [workflows](/guides/actions/) on the pull request, as for anyone's pull
178+ request. g1t waits for them to finish. Only the workflow runs report, so
179+ the agent has no say in the result.
173180 2. **Review.** A different agent reads the change and posts comments on
174181 lines, a summary and a verdict.
175−3. **Revision.** If the checks fail or the review asks for changes, the
176− author is sent back with exactly what was found, and steps 1 and 2 run
177− again on the result. This happens at most twice.
178−4. **Ready to merge.** Checks passed and approved. Merging is yours,
179− unless the repository says otherwise (below).
182+3. **Revision.** If a check fails or the review asks for changes, the
183+ author is sent back with exactly what was found. For a failed check,
184+ that is the end of the log of each failed job, up to three jobs and
185+ about 3,000 characters each; it can read more with `get_workflow_run`
186+ and `get_job_logs`. Steps 1 and 2 run again on the result. This happens
187+ at most twice, or as often as the repository's **Revisions before asking
188+ you** allows.
189+4. **Ready to merge.** The required checks passed and it is approved.
190+ Merging is yours, unless the repository says otherwise (below).
191+
192+Agents are told to run the same tests and linters the workflows run before
193+they finish, so most failures are caught in the sandbox. What "done" means
194+for the issue, if its description says so, is context for the agent and
195+its reviewer; what decides the merge is the default branch's
196+[required status checks](/guides/pull-requests/#required-status-checks).
197+A repository with no workflows has nothing to prove a change works; **Add
198+CI** gives it one ([add CI](/guides/actions/#add-ci)).
180199
181200 If `main` has moved in the meantime, that does not hold the pull request
182201 up. Merging it brings it up to date first: g1t merges `main` in, an agent
192211 it: the agent is sent to merge `main` in and resolve the conflicts, told
193212 which files conflict, and the checks run again on the result.
194213
195−The pull request's page shows which step it is at. If g1t cannot finish,
196−because the checks still fail after two revisions, a review could not be
197−written, or a conflict could not be resolved while bringing it up to date,
198−it stops and the page says
199−**Needs you**, with the reason. Pushing to the pull request yourself starts
200−it moving again.
214+The pull request's page shows which step it is at. While a required check
215+has not reported on its latest commit, it says so: "Waiting for the
216+required check CI to report on its latest commit." If g1t cannot finish,
217+because a required check still fails after the agent revised as often as
218+the repository allows ("The required check CI still fails after the agent
219+revised twice."), a review could not be written, or a conflict could not
220+be resolved while bringing it up to date, it stops and the page says
221+**Needs you**, with the reason. A check that is not required and still
222+fails after the last revision does not hold it. Pushing to the pull
223+request yourself starts it moving again.
201224
202225 ### What a repository can ask for
203226
204−Under a project's **Settings → Repository**, someone with the Maintain
205−[role](/guides/access-and-roles/) or higher sets the rules its pull requests follow:
227+Under a project's **Settings → Branches and merging**, someone with the
228+Maintain [role](/guides/access-and-roles/) or higher sets the rules its pull
229+requests follow. **Branch protection** holds the rules for every pull
230+request, a person's or an agent's; they are described in
231+[required status checks](/guides/pull-requests/#required-status-checks):
206232
207233 | Setting | Default | What it does |
208234 | --- | --- | --- |
209−| Require a pull request to change `main` | Off | Refuses pushes to the default branch. |
235+| Require a pull request to change the default branch | Off | Refuses pushes to the default branch. |
236+| Required status checks | None | The checks that must pass on a pull request's head before it merges. |
210237 | Required approvals | None | How many reviewers must approve before a merge. A reviewer who asked for changes blocks it. |
211238 | A g1t agent's approval counts | On | Off means approvals have to come from people. |
212−| Require acceptance checks to pass | Off | On means nobody can merge with failed checks. |
213−| Require pull requests to be up to date | Off | On means catching up is a step of its own and the checks run again. |
239+| Require branches to be up to date before merging | Off | On means catching up is a step of its own and the checks run again. |
240+| Merge through a queue | Off | Merging tests a pull request together with those ahead of it; the default branch only moves to a combination that passed. See [merge queue](/guides/merge-queue/). |
241+| Allow bypassing required checks | On | Lets someone who may merge merge without the required checks passing. Off means nobody can. |
242+
243+**g1t agents** holds what g1t does with its own agents' pull requests:
244+
245+| Setting | Default | What it does |
246+| --- | --- | --- |
214247 | Review by a second agent | On | Off leaves review to people. |
215248 | Revisions before asking you | 2 | How often an agent is sent back before g1t stops. |
216249 | Merge automatically when ready | Off | Lands a g1t agent's pull request once every rule is met. |
217250 | Ask a person before merging low-confidence changes | On | A g1t agent's change [rated low](#how-sure-the-agent-is) waits for a person's approval instead of merging by itself or joining the queue. |
218−| Merge through a queue | Off | Merging tests a pull request together with those ahead of it; `main` only moves to a combination that passed. See [merge queue](/guides/merge-queue/). |
219251
220252 A g1t agent's pull request follows the same rules as anyone's. If the
221253 repository wants approvals from people, it waits for them, and shows
234266
235267 A repository can land a g1t agent's pull request by itself once it is
236268 ready. Someone with the Maintain role or higher turns this on under the repository's
237−**Settings → Repository**; it is off to begin with. The merge is recorded as made by
269+**Settings → Branches and merging**; it is off to begin with. The merge is recorded as made by
238270 `g1t`, the issue closes naming the pull request, and nothing short of
239−ready is ever merged this way. One that is behind `main` is brought up to
271+ready is ever merged this way: every required check has passed on its
272+head. One that is behind `main` is brought up to
240273 date as part of the merge. Pull requests from people and from other
241274 agents always wait for a person to merge them.
242275
244277 **Session**, reviews in its conversation.
245278
246279 This applies to pull requests made by g1t agents. One you or your own
247−agent opened is yours to drive; the same checks run on it, and you can ask
280+agent opened is yours to drive; the same workflows run on it, and you can ask
248281 for a review or a catch-up from its page.
249282
250283 ### How sure the agent is
263296
264297 | Signal | Effect |
265298 | --- | --- |
266−| The acceptance checks fail, or could not run | Low |
299+| Required checks fail | Low |
300+| It failed in the [merge queue](/guides/merge-queue/) | Low |
267301 | The reviewer agent asks for changes | Low |
268302 | A run was stopped at its cost or time cap | Low |
269303 | Sent back to revise | 1 point per revision, at most 3 |
270−| The checks have not finished, or passed only on a retry | 1 point |
271−| The issue has no acceptance checks | 1 point |
304+| Required checks have not finished, or have not run on its head | 1 point |
305+| Checks passed only on a retry | 1 point |
306+| The default branch has no required checks | 1 point |
272307 | No review yet, or the repository has no reviewer agent | 1 point |
273308 | The reviewer approved but left three or more comments on lines | 1 point |
274309 | Code changed and no test was added or changed | 1 point |
287322 When the agent's word is lower, the reasons start with "agent says low",
288323 and the pull request lists what it was unsure about.
289324
290−For high confidence, the reasons say what it rests on: checks pass,
325+For high confidence, the reasons say what it rests on: required checks pass,
291326 approved on the first review, tests added, a small change.
292327
293328 The pull request's `confidence` in the
445480 an agent can build and test most projects. If your project needs something
446481 else, the agent will say in its summary what it could not run.
447482
483+A workspace (or a project) can send its agents' work to
484+[its own runners](/guides/self-hosted-runners/#agents-on-your-runners)
485+instead, so agents build and test with what those machines have. The agent
486+works the same way there, with the same short-lived credentials, and its
487+model calls still go through g1t; the machine time is free. g1t's network
488+guardrails cannot be enforced on your machines, and the run says so.
489+
448490 ## Who can run agents
449491
450492 Putting an agent to work (assigning it, mentioning it, asking it for a
461503 workspace has the whole forge, and two ways to try agents:
462504
463505 - **The trial.** $5 of usage, once per workspace, after a card check.
464−- **The open-source pool.** Checks, workflows and the merge queue on public
506+- **The open-source pool.** Workflows and the merge queue on public
465507 repositories, after the same card check. It does not pay for agents.
466508
467509 This holds whether the agent uses g1t's hosted models or
562604 | Catch up | Reads the repository; pushes to the pull request's fork or branch only | Records the session of its own pull request |
563605 | Review | Reads the change and the repository; pushes nothing | Reports its review through its own run |
564606 | Plan | Reads the repository; pushes nothing | Reports its plan through its own run, for a person to apply; it can create issues in its repository only |
565−| Checks, merge check | Reads the change; pushes nothing | None |
607+| Merge check | Reads the change; pushes nothing | None |
566608 | Merge queue | Reads each queued change; pushes the queue's own branch only | None |
567609 | Deploy | Reads the commit it builds; pushes nothing | None |
568610
+6−2
8080
8181 ## Protected branches
8282
83−A repository can protect its default branch under **Settings → Repository**. Pushing to
84−it is then refused for everyone, whatever their role, and for agents, and
83+A repository can protect its default branch under **Settings → Branches and
84+merging**. Pushing to it is then refused for everyone, whatever their role, and for agents, and
8585 git says why:
8686
8787 ```text
9191 Changes reach a protected branch only by merging a pull request. The first
9292 push to an empty repository is still allowed.
9393
94+The same page sets what a merge needs: the
95+[required status checks](/guides/pull-requests/#required-status-checks)
96+and approvals.
97+
9498 ## Branches
9599
96100 Push any branch to a repository you can write to, and open a
+42−5
1010
1111 They apply to every sandbox g1t starts for a project's agents (implement,
1212 revise, answer, catch up, review, plan, and replies to mentions). The
13−sandboxes of its acceptance checks and merge queue get the network list and
14−the time cap; they run the project's commands, not an agent, so command
15−rules and the cost cap do not apply to them. GitHub Actions jobs and deploy
13+sandboxes of its merge queue get the network list and the time cap; they
14+build the queue's states, not an agent's work, so command rules and the
15+cost cap do not apply to them. GitHub Actions jobs and deploy
1616 builds get the network list too, with what builds need added (see
1717 [builds](#builds)), and their own time limit. Merge checks are not covered;
1818 see [What is not covered](#what-is-not-covered). Every sandbox, whatever
2929
3030 Every setting on a project's page starts as "As the workspace", which
3131 follows the workspace's default, whatever it is now. Choose a value to
32−override it for that project only. Allowed domains and deny patterns add
33−up: a project's are added to the workspace's, never instead of them.
32+override it for that project only. Allowed domains, workflow-only domains
33+and deny patterns add up: a project's are added to the workspace's, never
34+instead of them.
3435
3536 Changes apply to runs that start after you save. A run that is under way
3637 keeps the guardrails it started with.
108109 project whose guardrails set **Only allowed hosts** to Open runs its jobs
109110 and builds with an open network too.
110111
112+### Workflow-only domains
113+
114+Some hosts only a workflow should reach: the API a deploy uploads to, a
115+release server, a package registry you publish to. Listing them under
116+allowed domains would open them to agents as well. List them under
117+**Workflow-only domains** instead, one per line:
118+
119+```
120+api.cloudflare.com | deploy.yml | production
121+uploads.example.com | release.yml, nightly.yml
122+*.internal.example.com
123+```
124+
125+| Part | |
126+| --- | --- |
127+| The domain | As for allowed domains: `example.com`, or `*.example.com` for its subdomains. |
128+| Workflows | Workflow files by name, comma-separated, as they are in `.g1t/workflows/`. Left out: any workflow. |
129+| Environments | The environments a job must name with `environment:`, comma-separated. Left out: any job. |
130+
131+A domain is reached only by:
132+
133+- jobs of the workflows and environments its line names;
134+- in a run that is not of a pull request from a fork, which runs code
135+ anyone could write.
136+
137+Agents, checks, the merge queue and deploy builds never reach these
138+hosts, whatever the line says. A job that names its environment with an
139+expression (`environment: ${{ inputs.target }}`) matches only lines with
140+no environments.
141+
142+Workflow-only domains are set by the same people as the rest of the page:
143+owners for the workspace's, Maintain or higher for a project's. Each change
144+is recorded in the workspace's [audit log](/guides/audit-log/) as
145+`update_guardrails`, saying which domains were added or removed and what
146+they were limited to.
147+
111148 ## Commands
112149
113150 The agent's harness checks every tool call the agent makes before it runs.
+1−1
1515 | --- | --- |
1616 | **Name** | The repository's name, the second part of its address. |
1717 | **Details** | Its description, website and topics, shown on its page, in [search and Explore](/guides/search/). |
18−| **Branches** | The default branch, renaming a branch, and a link to **Branches and merging**: [branch protection](/guides/git/#protected-branches), required approvals, the [merge queue](/guides/merge-queue/) and what agents do. What a sandbox may reach is under **Guardrails**; see [guardrails](/guides/guardrails/). |
18+| **Branches** | The default branch, renaming a branch, and a link to **Branches and merging**: [branch protection](/guides/git/#protected-branches), [required status checks](/guides/pull-requests/#required-status-checks), required approvals, the [merge queue](/guides/merge-queue/) and what agents do. What a sandbox may reach is under **Guardrails**; see [guardrails](/guides/guardrails/). |
1919 | **Danger zone** | Change visibility, archive, [transfer](/guides/transferring-repositories/) and delete. Shown to people with the Admin role. |
2020
2121 While a repository is [archived](#archive-a-repository), the settings that
+39−34
11 ---
22 title: Merge queue
3−description: Test each pull request together with the ones ahead of it, so main only moves to a state whose checks passed.
3+description: Test each pull request together with the ones ahead of it, so main only moves to a state whose required checks passed.
44 ---
55
66 Merging one pull request at a time, each caught up with `main`, keeps every
77 merge clean as text. It does not prove the result works: two changes can
88 merge without a conflict and still break each other. With the merge queue
99 on, a pull request is tested together with everything ahead of it before it
10−lands, and `main` only ever moves to a state whose checks passed.
10+lands, and `main` only ever moves to a state whose required checks passed.
1111
1212 The merge queue runs in g1t's sandboxes, which work in any workspace with
1313 [its own model provider](/guides/models/) and in those g1t's hosted models
1616
1717 ## Turn it on
1818
19−1. Open the project's **Settings → Repository**. You need the Maintain
19+1. Open the project's **Settings → Branches and merging**. You need the Maintain
2020 [role](/guides/access-and-roles/) or higher on its repository.
2121 2. Turn on **Merge through a queue**.
2222 3. Save.
23+4. Add `merge_group` to the `on:` of every workflow behind a
24+ [required status check](/guides/pull-requests/#required-status-checks),
25+ so that it runs on the queue's states too
26+ ([below](#what-each-state-is-held-to)).
2327
2428 From the API, send `merge_queue` to `PATCH /repos/{owner}/{name}/settings`
2529 (or `update_repo_settings`):
3741 the `pull_request` tool's `merge` action, or with
3842 `POST /repos/{owner}/{name}/pulls/{number}/merge`,
3943 adds it to the queue instead of changing `main`. Everything a merge needs
40−is still checked first: the pull request must be ready for review, its
41−checks must have passed and it must have the approvals the repository asks
44+is still checked first: the pull request must be ready for review, every
45+[required check](/guides/pull-requests/#required-status-checks) must have
46+passed on its head, and it must have the approvals the repository asks
4247 for. Only people with the Write [role](/guides/access-and-roles/) or higher can add to the
4348 queue. Merging a pull
4449 request that is already queued changes nothing.
6671
6772 ### What each state is held to
6873
69−Each tested state runs:
74+g1t pushes each state it built to a branch of its own, `g1t-queue/<entry>`,
75+and runs the repository's [workflows](/guides/actions/) that run on
76+`merge_group` on it. The entry waits for them, and passes only if:
7077
71−- the acceptance checks of every pull request in it; and
72−- the **contract**: the checks of issues already completed on the
73− repository, from the 30 most recently closed. Once an issue lands, its
74− checks become part of what `main` promises, and every later change is
75− held to them.
78+- every `merge_group` workflow it started passed; and
79+- every [required status check](/guides/pull-requests/#required-status-checks)
80+ of the default branch passed on that commit.
7681
77−So a change that breaks something that landed before it is caught here,
78−even when it merges without a conflict and its own checks pass.
82+So a change that breaks something another change ahead of it relies on is
83+caught here, even when it merges without a conflict and its own checks
84+passed. The branch is deleted once the entry lands or leaves the queue.
7985
80−Once those pass, the repository's [GitHub Actions](/guides/actions/)
81−workflows that run `on: merge_group` run on the state too, with the same
82−`merge_group` event GitHub sends, on the branch `g1t-queue/<entry>`. The
83−entry waits for them, and lands only if they pass. The branch is deleted
84−once the entry lands or leaves the queue. A workflow opts in like this:
86+A workflow opts in like this:
8587
8688 ```yaml
8789 on:
8991 merge_group:
9092 ```
9193
92−A contract check that fails is run again on `main` alone. If it fails there
93−too, it was broken already: it is marked as passing with a note, "already
94−failing on the default branch; not held against this", and does not hold
95−the change back.
94+Required checks only report on a queued state if their workflows run on
95+`merge_group`. When the branch requires checks and no workflow runs on
96+`merge_group`, the entry fails with a message saying so: "the required
97+check CI cannot report on it: no workflow runs on merge_group events. Add
98+merge_group to the on: of the workflows the branch requires". A required
99+check that a workflow did not report on the state fails it the same way.
100+A repository that requires no checks and has no `merge_group` workflows
101+only has each state built: an entry passes once it merges cleanly with
102+what is ahead of it.
96103
97104 ## How entries land
98105
118125
119126 ## When an entry fails
120127
121−An entry fails when its checks or its `merge_group` workflows fail in the
122−combined state, when it does not merge cleanly with what is ahead of it, or
128+An entry fails when its `merge_group` workflows or required checks fail
129+on the combined state, when it does not merge cleanly with what is ahead of it, or
123130 when the state cannot be built.
124131 It leaves the queue, and:
125132
126−1. Its pull request gets a failed check run. Each command is named with the
127− state it ran in, such as `cargo test (merge queue, on the default branch
128− with #41 merged in first)`, and the run says why it failed. A conflict
129− names the pull request ahead it collided with, and the files.
133+1. Its pull request records the failure, saying why: which workflow failed
134+ on the state, which required check did not report, or, for a conflict,
135+ the pull request ahead it collided with and the files.
130136 2. Its conversation records that it was taken out of the queue, and why: a
131137 conflict links the pull request it collided with and each conflicting
132138 file, which opens in the pull request's changes.
133139 3. The entries that were tested on top of it are tested again without it.
134140
135−A g1t agent's pull request is then sent back to revise, like any failed
141+A g1t agent's pull request is then sent back to revise, as for any failed
136142 check, starting from `main` as it is now. The revision counts towards
137143 **Revisions before asking you**. Once it is ready again, a repository with
138144 **Merge automatically when ready** on adds it to the queue again by itself;
152158 | --- | --- |
153159 | State | **Waiting**, **Testing** or **Passed**. |
154160 | Tested as | `main` and the pull requests merged into it, such as `main + #41 + #44`. |
155−| Checks | How many of the checks passed. |
161+| Checks | How its state's checks stand. |
156162 | Who | The agent or person who made the pull request, and who queued it. |
157163 | Commit | The tested state's commit. |
158164
159165 **Recently** lists the last 20 that left the queue: **Landed**, **Failed**
160−or **Removed**. A failed entry shows why, and the output of the checks that
161−failed.
166+or **Removed**. A failed entry shows why.
162167
163168 ## From the API or an agent
164169
199204 | `ahead` | The pull requests merged ahead of it in the state being tested. Empty when it was tested on `main` alone. |
200205 | `base_commit` | The commit of `main` the state was built on. |
201206 | `combined_commit` | The tested state. |
202−| `results` | The checks run against it, each with `command`, `passed` and `output`. |
203−| `error` | Why it failed: a conflict, or what could not be run. |
207+| `results` | What building the state recorded, each with `command`, `passed` and `output`. The workflow runs on it are on its commit, `combined_commit`. |
208+| `error` | Why it failed: a conflict, a workflow that failed on it, a required check that did not report, or what could not be built. |
204209 | `enqueued_by` | Who added it: a username, or `g1t` when it was merged automatically. |
+8−6
55
66 You do not have to split work into issues yourself. Write the outcome you
77 want on a repository's **Plan** page. An agent reads the repository and
8−proposes the issues that would get there, with acceptance checks and the
9−order they have to land in. You read the plan, keep what you want, and open
8+proposes the issues that would get there, with what done means for each and
9+the order they have to land in. You read the plan, keep what you want, and open
1010 it. g1t agents then work on the issues, as many at once as the dependencies
1111 allow, and the outcome page shows each one until it lands.
1212
4343 | --- | --- |
4444 | Title and labels | What the issue is. |
4545 | **Starts at once**, or **After** | Whether it depends on nothing, or which earlier issues have to merge first. |
46−| Acceptance checks | The commands a pull request for it must make pass, taken from how the repository is tested. |
46+| Definition of done | What has to be true for it to be done, in plain words. It is added to the issue's description under **Definition of done**, for the agent and its reviewer. What a pull request for it must pass to merge is the default branch's [required status checks](/guides/pull-requests/#required-status-checks). |
4747 | Files | The files it will most likely change. |
4848 | **What the agent will be told** | The issue's description, in full. An agent given the issue works from this text. |
4949
7171 it merged; and
7272 - the repository has room. At most six g1t agents make changes in one
7373 repository at once. The rest wait their turn, which also leaves sandboxes
74− free for checks and reviews.
74+ free for reviews.
7575
7676 Each queued issue says so in its conversation, for example "queued this for
7777 g1t-agent, to start once #41 has merged". From there each issue is
104104 | Waiting for an agent | Queued, and waiting for an agent to be free. |
105105 | Open | Nobody is working on it. |
106106 | Agent working | A g1t agent is making the change. |
107−| Checking | The acceptance checks are running. |
107+| Checking | Its workflows are running, or a required check has not reported yet. |
108108 | In review | A g1t agent is reviewing the change. |
109109 | Revising | The agent was sent back by the checks, a review or a person. |
110110 | Catching up | The agent is merging in the branch it will land on, which has moved. |
157157 ```
158158
159159 A plan's `status` is `planning`, `ready`, `failed` or `applied`. Each
160−proposed issue has `title`, `body`, `labels`, `checks`, `files`,
160+proposed issue has `title`, `body`, `labels`, `done` (what done means, in
161+plain words, added to `body` under **Definition of done** when it is
162+opened), `files`,
161163 `depends_on` (positions in the plan, counting from 1) and, once applied,
162164 `number`. `keep` takes positions counting from 1; leave it out to open
163165 every issue.
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

This change is too large to show in full.