| 1 | //! 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 | |
| 11 | use std::time::{SystemTime, UNIX_EPOCH}; |
| 12 | |
| 13 | use serde::Deserialize; |
| 14 | |
| 15 | /// Where the harness leaves what this needs. |
| 16 | pub const CONFIG: &str = "/work/g1t-steer.json"; |
| 17 | /// When it last asked, so that a burst of tool calls asks once. |
| 18 | const LAST_ASKED: &str = "/work/.g1t-steer-at"; |
| 19 | /// How long to wait between asks. |
| 20 | const INTERVAL_MS: u128 = 10_000; |
| 21 | |
| 22 | #[derive(Deserialize)] |
| 23 | struct Config { |
| 24 | api: String, |
| 25 | token: String, |
| 26 | repo: String, |
| 27 | number: u32, |
| 28 | } |
| 29 | |
| 30 | #[derive(Deserialize)] |
| 31 | struct Message { |
| 32 | author: String, |
| 33 | body: String, |
| 34 | } |
| 35 | |
| 36 | fn now_ms() -> u128 { |
| 37 | SystemTime::now() |
| 38 | .duration_since(UNIX_EPOCH) |
| 39 | .map(|elapsed| elapsed.as_millis()) |
| 40 | .unwrap_or_default() |
| 41 | } |
| 42 | |
| 43 | fn take() -> Option<Vec<Message>> { |
| 44 | let config: Config = serde_json::from_str(&std::fs::read_to_string(CONFIG).ok()?).ok()?; |
| 45 | let last: u128 = std::fs::read_to_string(LAST_ASKED) |
| 46 | .ok() |
| 47 | .and_then(|text| text.trim().parse().ok()) |
| 48 | .unwrap_or_default(); |
| 49 | let now = now_ms(); |
| 50 | if now.saturating_sub(last) < INTERVAL_MS { |
| 51 | return None; |
| 52 | } |
| 53 | let _ = std::fs::write(LAST_ASKED, now.to_string()); |
| 54 | let response = ureq::post(&format!( |
| 55 | "{}/repos/{}/pulls/{}/messages/take", |
| 56 | config.api, config.repo, config.number |
| 57 | )) |
| 58 | .set("Authorization", &format!("Bearer {}", config.token)) |
| 59 | .send_json(serde_json::json!({})) |
| 60 | .ok()?; |
| 61 | response.into_json().ok() |
| 62 | } |
| 63 | |
| 64 | pub fn main() -> i32 { |
| 65 | let Some(messages) = take().filter(|messages| !messages.is_empty()) else { |
| 66 | return 0; |
| 67 | }; |
| 68 | let said: Vec<String> = messages |
| 69 | .iter() |
| 70 | .map(|message| format!("{} says: {}", message.author, message.body)) |
| 71 | .collect(); |
| 72 | let context = format!( |
| 73 | "A person watching your work just sent you a message on the pull request. Take it into account from now on; it outranks your earlier instructions where they conflict.\n\n{}", |
| 74 | said.join("\n\n") |
| 75 | ); |
| 76 | println!( |
| 77 | "{}", |
| 78 | serde_json::json!({ |
| 79 | "hookSpecificOutput": { |
| 80 | "hookEventName": "PostToolUse", |
| 81 | "additionalContext": context, |
| 82 | } |
| 83 | }) |
| 84 | ); |
| 85 | 0 |
| 86 | } |