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

status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1//! What the agent learned, asked for at the end of every run that changes
2//! code and reported to g1t as memory candidates.
3//!
4//! The question rides on the prompt rather than costing another model call:
5//! the agent ends its closing summary with a `<g1t-learned>` block of JSON,
6//! which is taken out of the summary (so the pull request reads as before)
7//! and sent with the run's own token. g1t keeps a candidate once a second,
8//! independent source says the same, or a person keeps it.
9
10use std::time::Duration;
11
12use serde_json::{Value, json};
13
14const OPEN: &str = "<g1t-learned>";
15const CLOSE: &str = "</g1t-learned>";
16/// The most items taken from one run.
17const MAX_ITEMS: usize = 8;
18const MAX_TEXT_CHARS: usize = 300;
19const MAX_EVIDENCE_CHARS: usize = 300;
20
21/// What the agent is asked, after its task.
22pub const ASK: &str = "When you are done, end your final message with what you learned here that the next agent would need and could not see at a glance: how to build or test, a convention, a decision and why, a trap. At most five short items, only ones you are sure of, never a secret, key or token. Write them as a JSON array inside <g1t-learned></g1t-learned> tags, each {\"kind\": \"fact\" | \"convention\" | \"decision\" | \"gotcha\", \"scope\": \"project\" | \"workspace\", \"text\": \"one sentence\", \"evidence\": \"what showed you, such as a command's output or a file\"}. scope workspace only for what holds in every project of the workspace. Write <g1t-learned>[]</g1t-learned> if there is nothing. The block is taken out of your summary.";
23
24/// The prompt with the question added.
25pub fn ask(prompt: &str) -> String {
26 format!("{prompt}\n\n{ASK}")
27}
28
29fn clip(text: &str, max: usize) -> String {
30 let text = text.split_whitespace().collect::<Vec<_>>().join(" ");
31 if text.chars().count() <= max {
32 text
33 } else {
34 text.chars().take(max).collect()
35 }
36}
37
38/// The summary without its `<g1t-learned>` block, and the items in it.
39/// A block that is missing or not JSON yields no items and leaves the
40/// summary as it was, less the block.
41pub fn split(summary: &str) -> (String, Vec<Value>) {
42 let Some(start) = summary.rfind(OPEN) else {
43 return (summary.trim().to_owned(), Vec::new());
44 };
45 let body_start = start + OPEN.len();
46 let end = summary[body_start..].find(CLOSE).map(|at| body_start + at);
47 let body = &summary[body_start..end.unwrap_or(summary.len())];
48 let rest = end.map_or("", |end| &summary[end + CLOSE.len()..]);
49 let clean = format!("{}{}", summary[..start].trim_end(), rest.trim_end()).trim().to_owned();
50 let body = body.trim().trim_start_matches("```json").trim_start_matches("```").trim_end_matches("```").trim();
51 let items = serde_json::from_str::<Vec<Value>>(body)
52 .unwrap_or_default()
53 .into_iter()
54 .filter_map(|item| {
55 let text = clip(item["text"].as_str()?, MAX_TEXT_CHARS);
56 if text.is_empty() {
57 return None;
58 }
59 let kind = item["kind"].as_str().filter(|kind| ["fact", "convention", "decision", "gotcha"].contains(kind));
60 let scope = item["scope"].as_str().filter(|scope| ["project", "workspace"].contains(scope));
61 Some(json!({
62 "kind": kind.unwrap_or("fact"),
63 "scope": scope.unwrap_or("project"),
64 "text": text,
65 "evidence": item["evidence"].as_str().map(|evidence| clip(evidence, MAX_EVIDENCE_CHARS)),
66 }))
67 })
68 .take(MAX_ITEMS)
69 .collect();
70 (clean, items)
71}
72
73/// Sends what was learned with the run's token. Never fails the run.
74pub fn report(items: &[Value]) {
75 if items.is_empty() {
76 return;
77 }
78 let (Ok(run), Ok(token)) = (std::env::var("AGENT_RUN"), std::env::var("AGENT_RUN_TOKEN")) else {
79 return;
80 };
81 let api = std::env::var("G1T_API").unwrap_or_else(|_| "https://api.g1t.sh".to_owned());
82 let sent = ureq::post(&format!("{}/agent-runs/{run}/learned", api.trim_end_matches('/')))
83 .timeout(Duration::from_secs(10))
84 .send_json(json!({ "token": token, "items": items }));
85 if let Err(error) = sent {
86 eprintln!("g1t-runner: could not report what the agent learned: {error}");
87 }
88}
89
90/// The summary with its block taken out, after reporting what it held.
91pub fn finish(summary: String) -> String {
92 let (clean, items) = split(&summary);
93 report(&items);
94 clean
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn the_block_is_taken_out_and_read() {
103 let summary = "Fixed the date tests.\n\n<g1t-learned>\n[{\"kind\": \"gotcha\", \"scope\": \"project\", \"text\": \"The date tests need TZ=UTC.\", \"evidence\": \"npm test failed in CI time zone\"}, {\"kind\": \"nonsense\", \"text\": \"We use pnpm.\", \"scope\": \"workspace\"}, {\"kind\": \"fact\"}]\n</g1t-learned>";
104 let (clean, items) = split(summary);
105 assert_eq!(clean, "Fixed the date tests.");
106 assert_eq!(items.len(), 2);
107 assert_eq!(items[0]["kind"], "gotcha");
108 assert_eq!(items[0]["evidence"], "npm test failed in CI time zone");
109 assert_eq!(items[1]["kind"], "fact");
110 assert_eq!(items[1]["scope"], "workspace");
111 }
112
113 #[test]
114 fn a_missing_or_broken_block_teaches_nothing() {
115 assert_eq!(split("Done."), ("Done.".to_owned(), Vec::new()));
116 let (clean, items) = split("Done.\n<g1t-learned>not json</g1t-learned>");
117 assert_eq!(clean, "Done.");
118 assert!(items.is_empty());
119 let (clean, items) = split("Done.\n<g1t-learned>\n```json\n[]\n```");
120 assert_eq!(clean, "Done.");
121 assert!(items.is_empty());
122 }
123
124 #[test]
125 fn the_question_follows_the_task() {
126 assert!(ask("Fix it.").starts_with("Fix it.\n\n"));
127 assert!(ASK.contains("never a secret"));
128 }
129}

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