g1t/crates/runner/src/main.rs

138 lines4,754 bytesCodeBlame

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.

Issues and pull requests replace intents and attempts1//! Runs a coding agent on one pull request and reports back to g1t.
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)2//!
Issues and pull requests replace intents and attempts3//! This is the program a hosted sandbox starts. It clones the pull
4//! request's fork, runs the agent harness headless, streams what the agent
5//! does into the pull request's session as it happens, pushes the result
6//! and marks the pull request ready for review. It talks to g1t only through the public API and git, exactly
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)7//! as an agent on someone's own machine would.
8//!
9//! Configuration comes from the environment:
10//!
11//! - `G1T_API`, `G1T_TOKEN`, `G1T_USER`: where and who to report as.
Issues and pull requests replace intents and attempts12//! - `G1T_REPO`, `PULL_NUMBER`, `GIT_REMOTE`: the pull request and its fork.
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)13//! - `PROMPT`: what the agent is asked to do.
14//! - `COMMIT_MESSAGE`: used if the agent leaves changes uncommitted.
15//! - `ANTHROPIC_API_KEY`: read by the harness itself.
16
17mod harness;
18mod report;
19
20use std::path::Path;
21use std::process::Command;
22
23use anyhow::{Context, Result, bail};
24use base64::Engine;
25use base64::engine::general_purpose::STANDARD;
26
27use report::{Entry, Reporter};
28
29const WORKDIR: &str = "/work/repo";
30
31fn env(name: &str) -> Result<String> {
32 std::env::var(name).with_context(|| format!("{name} is not set"))
33}
34
35/// Runs git and returns its trimmed output, failing on a non-zero exit.
36fn git(dir: &Path, args: &[&str]) -> Result<String> {
37 let output = Command::new("git")
38 .current_dir(dir)
39 .args(args)
40 .output()
41 .context("could not run git")?;
42 if !output.status.success() {
43 bail!(
44 "git {} failed: {}",
45 args.first().unwrap_or(&""),
46 String::from_utf8_lossy(&output.stderr).trim()
47 );
48 }
49 Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
50}
51
52/// A git option that authenticates one command. The credential is passed
53/// per command and never written to the clone's config or its remote URL,
54/// where the agent would find it.
55fn auth_option(user: &str, token: &str) -> String {
56 let credentials = STANDARD.encode(format!("{user}:{token}"));
57 format!("http.extraHeader=Authorization: Basic {credentials}")
58}
59
60fn run(reporter: &mut Reporter) -> Result<String> {
61 let prompt = env("PROMPT")?;
62 let remote = env("GIT_REMOTE")?;
63 let auth = auth_option(&env("G1T_USER")?, &env("G1T_TOKEN")?);
64 let workdir = Path::new(WORKDIR);
65
Show the model behind each choice; toolchains in the sandbox66 if let Ok(model) = std::env::var("AGENT_MODEL_NAME") {
67 reporter.record(Entry::new("note", &format!("Running on {model}.")));
68 }
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)69 reporter.record(Entry::new("prompt", &prompt));
70 reporter.flush();
71
72 std::fs::create_dir_all("/work")?;
73 git(
74 Path::new("/work"),
75 &["-c", &auth, "clone", "--quiet", &remote, WORKDIR],
76 )
Issues and pull requests replace intents and attempts77 .context("could not clone the pull request's fork")?;
Diffs on attempts; hosted agent presented as the g1t agent78 git(workdir, &["config", "user.name", "g1t agent"])?;
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)79 git(workdir, &["config", "user.email", "agent@g1t.sh"])?;
80 let branch = git(workdir, &["rev-parse", "--abbrev-ref", "HEAD"])?;
81 let start = git(workdir, &["rev-parse", "HEAD"]).unwrap_or_default();
82
83 let summary = harness::run_claude(workdir, &prompt, reporter)?;
84
85 // Commit whatever the agent left in the working tree.
86 if !git(workdir, &["status", "--porcelain"])?.is_empty() {
87 let message = std::env::var("COMMIT_MESSAGE").unwrap_or_else(|_| "Agent changes".into());
88 git(workdir, &["add", "--all"])?;
89 git(workdir, &["commit", "--quiet", "--message", &message])?;
90 }
91 let head = git(workdir, &["rev-parse", "HEAD"])?;
92 if head == start {
93 bail!("the agent finished without changing anything");
94 }
95 git(
96 workdir,
97 &[
98 "-c",
99 &auth,
100 "push",
101 "--quiet",
102 "origin",
103 &format!("HEAD:{branch}"),
104 ],
105 )
Issues and pull requests replace intents and attempts106 .context("could not push the pull request's commits")?;
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)107 reporter.record(Entry::new(
108 "note",
Issues and pull requests replace intents and attempts109 &format!("Pushed {}.", &head[..head.len().min(12)]),
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)110 ));
111 Ok(summary)
112}
113
114fn main() {
115 let mut reporter = match Reporter::from_env() {
116 Ok(reporter) => reporter,
117 Err(error) => {
118 eprintln!("g1t-runner: {error:#}");
119 std::process::exit(2);
120 }
121 };
122 match run(&mut reporter) {
123 Ok(summary) => {
124 reporter.flush();
Issues and pull requests replace intents and attempts125 if let Err(error) = reporter.ready(&summary) {
126 eprintln!("g1t-runner: could not mark the pull request ready: {error:#}");
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)127 std::process::exit(1);
128 }
129 }
130 Err(error) => {
131 eprintln!("g1t-runner: {error:#}");
132 reporter.record(Entry::new("note", &format!("The run failed: {error:#}")));
133 reporter.flush();
Issues and pull requests replace intents and attempts134 let _ = reporter.close();
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)135 std::process::exit(1);
136 }
137 }
138}