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
116 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.

Money in billing's messages reads to the cent, and the deploy reads migrations again after a failure1//! Delivers people's messages to the agent while it works.
2//!
3//! Claude Code runs this as a hook after each of the agent's tool calls
4//! (see `harness`). It asks g1t for messages the agent has not seen and,
5//! if there are any, hands them to the agent as context for its next step.
6//! It never fails the agent's run: any problem means no message this time.
7//!
8//! What it needs is in `/work/g1t-steer.json`, written by the harness:
9//! the API, the agent's token, the repository and the pull request.
10
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use serde::Deserialize;
14
15/// Where the harness leaves what this needs.
16pub const CONFIG: &str = "/work/g1t-steer.json";
17/// When it last asked, so that a burst of tool calls asks once.
18const LAST_ASKED: &str = "/work/.g1t-steer-at";
19/// How long to wait between asks.
20const INTERVAL_MS: u128 = 5_000;
21
22#[derive(Deserialize)]
23struct Config {
24 api: String,
25 token: String,
26 repo: String,
27 number: u32,
28}
29
30/// One message, as the API sends it: `snake_case`, or `fromNumber` from
31/// before its bodies were.
32#[derive(Deserialize)]
33struct Message {
34 id: String,
35 author: String,
36 body: String,
37 #[serde(default)]
38 kind: String,
39 #[serde(default, alias = "fromNumber")]
40 from_number: Option<u32>,
41}
42
43/// One message as the agent should read it, with how to reply where it can.
44fn told(message: &Message) -> String {
45 let from = message
46 .from_number
47 .map_or_else(|| message.author.clone(), |number| format!("The agent on #{number}"));
48 match message.kind.as_str() {
49 "question" => format!(
50 "{from} asks you (message {}): {}\nAnswer it with the answer_message tool and that id.",
51 message.id, message.body
52 ),
53 "handoff" => format!(
54 "{from} hands you work that belongs in your pull request (message {}): {}\nTake it on, or decline it if it is not yours, with the answer_message tool and that id.",
55 message.id, message.body
56 ),
57 "answer" => format!("{from} answered you: {}", message.body),
58 _ => format!("{} says: {}", message.author, message.body),
59 }
60}
61
62fn now_ms() -> u128 {
63 SystemTime::now()
64 .duration_since(UNIX_EPOCH)
65 .map(|elapsed| elapsed.as_millis())
66 .unwrap_or_default()
67}
68
69/// The undelivered messages. Between steps, asks at most every few
70/// seconds; when the agent is about to stop, always.
71fn take(stopping: bool) -> Option<Vec<Message>> {
72 let config: Config = serde_json::from_str(&std::fs::read_to_string(CONFIG).ok()?).ok()?;
73 let last: u128 = std::fs::read_to_string(LAST_ASKED)
74 .ok()
75 .and_then(|text| text.trim().parse().ok())
76 .unwrap_or_default();
77 let now = now_ms();
78 if !stopping && now.saturating_sub(last) < INTERVAL_MS {
79 return None;
80 }
81 let _ = std::fs::write(LAST_ASKED, now.to_string());
82 let response = ureq::post(&format!(
83 "{}/repos/{}/pulls/{}/messages/take",
84 config.api, config.repo, config.number
85 ))
86 .set("Authorization", &format!("Bearer {}", config.token))
87 .send_json(serde_json::json!({}))
88 .ok()?;
89 response.into_json().ok()
90}
91
92pub fn main() -> i32 {
93 // Run as the Stop hook too, so a message sent while the agent was
94 // finishing is not lost: it keeps the agent going to act on it.
95 let stopping = std::env::var("G1T_HOOK").as_deref() == Ok("stop");
96 let Some(messages) = take(stopping).filter(|messages| !messages.is_empty()) else {
97 return 0;
98 };
99 let said: Vec<String> = messages.iter().map(told).collect();
100 let context = format!(
101 "New messages on your pull request. A person's outranks your earlier instructions where they conflict; another agent's is a colleague's.\n\n{}",
102 said.join("\n\n")
103 );
104 let output = if stopping {
105 serde_json::json!({ "decision": "block", "reason": context })
106 } else {
107 serde_json::json!({
108 "hookSpecificOutput": {
109 "hookEventName": "PostToolUse",
110 "additionalContext": context,
111 }
112 })
113 };
114 println!("{output}");
115 0
116}

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