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.

g1t/crates/runner/src/reply.rs

99 lines4,509 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.

g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1//! Answers a question someone asked `@g1t` in a comment, in the
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2//! 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
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent6//! to a file. This program posts the answer as `g1t`, with the
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API7//! 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.
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent12//! - `G1T_AGENT_TOKEN`: g1t's token for this run, to post the answer.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API13//! - `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";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API25const 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")?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents37 crate::clone::clone(Path::new("/work"), &auth, &[], &remote, WORKDIR).context("could not clone the repository")?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API38 if let Ok(reference) = env("GIT_REF")
39 && !reference.is_empty()
40 {
Fast pages, required checks on the branch, self-hosted runners, honest incidents41 // 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 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API46 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}