Skip to content

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

g1t.sh
99 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Billing's usage total is labeled at price, and Usage says what part of it was paid for1/**
2 * g1t's own work on a workspace's self-hosted runners. When a workspace (or
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.3 * one of its repositories) says so under Settings, Runners, a merge check or
4 * a state of the merge queue is not started in a sandbox here: the
5 * sandbox's Durable Object hands the same environment to the actions
6 * service as a task, a runner of the workspace's with the right labels
7 * takes it on its next poll, and the actions service tells the Durable
8 * Object how it ended (`task_ended`), which then does exactly what it does
Merge branch 'main' into ask-integration9 * when a container stops. The run says where it went.
Billing's usage total is labeled at price, and Usage says what part of it was paid for10 */
11
12import type { RepoPath, ServiceBinding } from "@g1t/contracts";
13
14/** The actions service's JSON protocol. */
15async function call<T>(actions: ServiceBinding, method: string, args: object): Promise<T> {
16 const response = await actions.fetch(`https://actions/rpc/${method}`, {
17 method: "POST",
18 headers: { "content-type": "application/json" },
19 body: JSON.stringify(args),
20 });
21 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
22 return (await response.json()) as T;
23}
24
25/**
26 * The labels g1t's own work in `repo` runs on, when it runs on the
27 * workspace's runners; null for g1t's sandboxes. Never throws: when the
28 * actions service cannot say, the work runs in a sandbox as before.
29 */
30export async function selfHostedRoute(actions: ServiceBinding, repo: RepoPath): Promise<string[] | null> {
31 try {
32 const labels = await call<string[] | null>(actions, "runner_route", { workspace: repo.namespace.toLowerCase(), repo });
33 return Array.isArray(labels) && labels.length > 0 ? labels : null;
34 } catch (error) {
35 console.log("self-hosted route unknown; using a sandbox", repo.namespace, String(error));
36 return null;
37 }
38}
39
40type Outcome<T> = { ok: true; value: T } | { ok: false; error: { message: string } };
41
42/** Hands a sandbox's work to the workspace's runners. Returns the task's id. */
43export async function enqueueTask(
44 actions: ServiceBinding,
45 task: {
46 sandbox: string;
47 repo: RepoPath;
48 kind: string;
49 title: string;
50 labels: string[];
51 env: Record<string, string>;
52 timeoutMinutes: number;
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.53 /** The run the work is, when the sandbox opened one: the Runners page links to it. */
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.54 runId?: string | null;
Billing's usage total is labeled at price, and Usage says what part of it was paid for55 },
56): Promise<string> {
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.57 const { runId, ...rest } = task;
58 const queued = await call<Outcome<string>>(actions, "enqueue_task", { ...rest, run_id: runId ?? null, workspace: task.repo.namespace.toLowerCase() });
Billing's usage total is labeled at price, and Usage says what part of it was paid for59 if (!queued.ok) throw new Error(queued.error.message);
60 return queued.value;
61}
62
63/** Withdraws a sandbox's task: a person stopped it, or its time ran out. Never throws. */
64export async function cancelTask(actions: ServiceBinding, sandbox: string, reason: string | null): Promise<void> {
65 await call(actions, "cancel_task", { sandbox, reason }).catch((error: unknown) =>
66 console.log("self-hosted task not cancelled", sandbox, String(error)),
67 );
68}
69
70/**
71 * The environment a self-hosted runner gets: what the sandbox would have
72 * been started with, less what only makes sense in g1t's sandbox (the
73 * egress certificate and proxy), with the mining watch off, since the
74 * machine and its electricity are the workspace's.
75 */
76export function taskEnv(vars: Record<string, string>): Record<string, string> {
77 const env: Record<string, string> = {};
78 for (const [name, value] of Object.entries(vars)) {
79 if (/^(HTTPS?_PROXY|NO_PROXY|NODE_EXTRA_CA_CERTS|SSL_CERT_FILE|G1T_EGRESS_CA)$/i.test(name)) continue;
80 env[name] = value;
81 }
82 env.G1T_ABUSE = "off";
83 env.G1T_SELF_HOSTED = "1";
84 return env;
85}
86
87/** What the run's session says when it goes to the workspace's runners. */
88export function handedOverStep(labels: string[]): string {
Merge branch 'main' into ask-integration89 return `Handed to a self-hosted runner with labels ${labels.join(", ")}.`;
Billing's usage total is labeled at price, and Usage says what part of it was paid for90}
91
92/** Where the work is: the tracked repository, else the meter's `owner/name`. */
93export function taskRepo(track: { repo: RepoPath } | undefined, meter: { repo: string } | undefined, owner: { repo: string | null } | undefined): RepoPath | null {
94 if (track) return track.repo;
95 const full = meter?.repo ?? owner?.repo ?? null;
96 if (!full) return null;
97 const [namespace, name] = full.split("/");
98 return namespace && name ? { namespace, name } : null;
99}

This file's history is long; its oldest lines are credited to the oldest commit read.