Skip to content
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.

The runner base keeps its old comment, so the pushed base image still matches its folder1//! Answers a question someone asked `@g1t` in a comment, in the
2//! thread it was asked in, changing nothing.
3//!
4//! The agent reads the repository as it is (the default branch for an
5//! issue, the pull request's head for a pull request) and writes its answer
6//! to a file. This program posts the answer as `g1t`, with the
7//! agent's own token, which the agent itself never holds.
8//!
9//! Configuration comes from the environment:
10//!
11//! - `G1T_API`, `G1T_REPO`, `REPLY_NUMBER`: where the question was asked.
12//! - `G1T_AGENT_TOKEN`: g1t's token for this run, to post the answer.
13//! - `GIT_REMOTE`, `GIT_REF`: what to read, and at which branch or commit.
14//! - `G1T_USER`, `G1T_TOKEN`: to read it, if it is private.
15//! - `PROMPT`: the question and what the agent is told around it.
16
17use std::path::Path;
18
19use anyhow::{Context, Result, bail};
20
21use crate::report::Reporter;
22use crate::{WORKDIR, auth_option, env, git, harness};
23
Reviews, plans and replies are written where the guardrail allows: g1t's answer files are inside it24pub(crate) const ANSWER_FILE: &str = "/work/answer.md";
The runner base keeps its old comment, so the pushed base image still matches its folder25const MAX_ANSWER_CHARS: usize = 20_000;
26
27const INSTRUCTIONS: &str = "Write your answer to /work/answer.md as Markdown, addressed to whoever asked: \
28plain sentences, specific, naming files and functions where that helps, no headings and no emoji. \
29Read the code before you answer; say so when you are not sure. Do not change any file in the repository, \
30and do not post the answer with add_comment: it is posted in the thread for you. Then finish.";
31
32fn answer() -> Result<String> {
33 let remote = env("GIT_REMOTE")?;
34 let auth = auth_option(&env("G1T_USER")?, &env("G1T_TOKEN")?);
35 let workdir = Path::new(WORKDIR);
36 std::fs::create_dir_all("/work")?;
37 crate::clone::clone(Path::new("/work"), &auth, &[], &remote, WORKDIR).context("could not clone the repository")?;
38 if let Ok(reference) = env("GIT_REF")
39 && !reference.is_empty()
40 {
41 // A commit the clone may not have fetched by name: fetch it, and
42 // everything if that is refused.
43 if crate::clone::fetch(workdir, &auth, "origin", &reference).is_err() && crate::clone::is_shallow(workdir) {
44 let _ = git(workdir, &["-c", &auth, "fetch", "--quiet", "--unshallow", "origin"]);
45 }
46 git(workdir, &["checkout", "--quiet", "--detach", &reference])
47 .or_else(|_| git(workdir, &["checkout", "--quiet", "--detach", "FETCH_HEAD"]))
48 .context("could not check out what the question is about")?;
49 }
50 let prompt = format!("{}\n\n{INSTRUCTIONS}", env("PROMPT")?);
51 // Steps show on the agent run; the answer is what matters here.
52 let mut reporter = Reporter::silent();
53 let summary = harness::run_claude(workdir, &prompt, &mut reporter)?;
54 let written = std::fs::read_to_string(ANSWER_FILE).unwrap_or_default();
55 let answer = if written.trim().is_empty() { summary } else { written };
56 let answer: String = answer.trim().chars().take(MAX_ANSWER_CHARS).collect();
57 if answer.is_empty() {
58 bail!("the agent wrote no answer");
59 }
60 Ok(answer)
61}
62
63pub fn main() -> i32 {
64 let (Ok(api), Ok(repo), Ok(number), Ok(token)) = (
65 env("G1T_API"),
66 env("G1T_REPO"),
67 env("REPLY_NUMBER"),
68 env("G1T_AGENT_TOKEN"),
69 ) else {
70 eprintln!("g1t-runner: G1T_API, G1T_REPO, REPLY_NUMBER and G1T_AGENT_TOKEN must be set");
71 return 2;
72 };
73 let secrets: Vec<String> = ["G1T_TOKEN", "G1T_AGENT_TOKEN", "ANTHROPIC_API_KEY", "BILLING_TOKEN", "AGENT_RUN_TOKEN"]
74 .iter()
75 .filter_map(|name| std::env::var(name).ok())
76 .filter(|secret| !secret.is_empty())
77 .collect();
78 let outcome = answer();
79 let body = match &outcome {
80 Ok(answer) => answer.clone(),
81 Err(error) => {
82 eprintln!("g1t-runner: {error:#}");
83 "I could not answer this: the run failed before I had an answer. Its steps are on the Agents page.".to_owned()
84 }
85 };
86 // Whatever the agent wrote passes through here, so nothing it could
87 // have read from its environment leaves in the answer.
88 let body = secrets
89 .iter()
90 .fold(body, |text, secret| text.replace(secret, "[redacted]"));
91 let posted = ureq::post(&format!("{api}/repos/{repo}/issues/{number}/comments"))
92 .set("Authorization", &format!("Bearer {token}"))
93 .send_json(serde_json::json!({ "body": body }));
94 if let Err(error) = posted {
95 eprintln!("g1t-runner: could not post the answer: {error:#}");
96 return 1;
97 }
98 i32::from(outcome.is_err())
99}

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