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.

Commit

Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API

- Guardrails: per-workspace and per-project network allowlist enforced outside the sandbox, command rules via a managed hook, cost and time caps that halt a run and leave its pull request for a person. Fork checkouts start without the fork's settings, hooks, MCP config, commands or CLAUDE.md. - Run credentials: each sandbox gets least-privilege tokens bound to its run, acting for the person who started it, intersected with their current membership; revoked when the run ends. Enforced in identity, the API/MCP and git. - Audit log: every agent action and every write, with on-behalf-of and refusal rules; workspace page, CSV/JSON export, "What it did" on runs. - Security: push protection for secrets, a history scan, dependency vulnerabilities from OSV opening upgrade issues for g1t-agent, and a Security page per project and workspace. - Context hub: catalog of projects, apps, packages, owners and environments with relations; memory capture with review; backfill; search_context and get_entity for people and agents; run context. - Repository instructions (AGENTS.md, CLAUDE.md, .g1t/review.md) from trusted branches only; @g1t-agent mentions and a label rule. - Mission control and the project overview rebuilt around what moved and what needs you. - Public API bodies, MCP results and webhook payloads are snake_case. - Docs link to the source on g1t, never GitHub; the current sidebar link's mark is a straight line.

syntaqxcommitted Parent53719c3Browse files
178 files+5438−10370/178 viewed
+73−4
989989 "futures-util",
990990 "g1t-contracts",
991991 "g1t-kit",
992+ "g1t-scan",
992993 "serde",
993994 "serde_json",
994995 "similar",
10121013 ]
10131014
10141015 [[package]]
1016+name = "g1t-scan"
1017+version = "0.1.0"
1018+dependencies = [
1019+ "miniz_oxide",
1020+ "serde",
1021+ "serde_json",
1022+ "sha1",
1023+ "sha2 0.10.9",
1024+ "similar",
1025+]
1026+
1027+[[package]]
10151028 name = "g1t-secrets"
10161029 version = "0.1.0"
10171030 dependencies = [
10241037 ]
10251038
10261039 [[package]]
1040+name = "g1t-security"
1041+version = "0.1.0"
1042+dependencies = [
1043+ "futures-util",
1044+ "g1t-contracts",
1045+ "g1t-kit",
1046+ "g1t-scan",
1047+ "getrandom 0.2.17",
1048+ "serde",
1049+ "serde_json",
1050+ "worker",
1051+]
1052+
1053+[[package]]
10271054 name = "g1t-sshd"
10281055 version = "0.1.0"
10291056 dependencies = [
17521779
17531780 [[package]]
17541781 name = "openssl-probe"
1782+version = "0.1.6"
1783+source = "registry+https://github.com/rust-lang/crates.io-index"
1784+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
1785+
1786+[[package]]
1787+name = "openssl-probe"
17551788 version = "0.2.1"
17561789 source = "registry+https://github.com/rust-lang/crates.io-index"
17571790 checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
23752408
23762409 [[package]]
23772410 name = "rustls-native-certs"
2411+version = "0.7.3"
2412+source = "registry+https://github.com/rust-lang/crates.io-index"
2413+checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
2414+dependencies = [
2415+ "openssl-probe 0.1.6",
2416+ "rustls-pemfile",
2417+ "rustls-pki-types",
2418+ "schannel",
2419+ "security-framework 2.11.1",
2420+]
2421+
2422+[[package]]
2423+name = "rustls-native-certs"
23782424 version = "0.8.4"
23792425 source = "registry+https://github.com/rust-lang/crates.io-index"
23802426 checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
23812427 dependencies = [
2382− "openssl-probe",
2428+ "openssl-probe 0.2.1",
23832429 "rustls-pki-types",
23842430 "schannel",
2385− "security-framework",
2431+ "security-framework 3.7.0",
2432+]
2433+
2434+[[package]]
2435+name = "rustls-pemfile"
2436+version = "2.2.0"
2437+source = "registry+https://github.com/rust-lang/crates.io-index"
2438+checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
2439+dependencies = [
2440+ "rustls-pki-types",
23862441 ]
23872442
23882443 [[package]]
24072462 "log",
24082463 "once_cell",
24092464 "rustls",
2410− "rustls-native-certs",
2465+ "rustls-native-certs 0.8.4",
24112466 "rustls-platform-verifier-android",
24122467 "rustls-webpki",
2413− "security-framework",
2468+ "security-framework 3.7.0",
24142469 "security-framework-sys",
24152470 "webpki-root-certs",
24162471 "windows-sys 0.61.2",
25082563
25092564 [[package]]
25102565 name = "security-framework"
2566+version = "2.11.1"
2567+source = "registry+https://github.com/rust-lang/crates.io-index"
2568+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
2569+dependencies = [
2570+ "bitflags",
2571+ "core-foundation 0.9.4",
2572+ "core-foundation-sys",
2573+ "libc",
2574+ "security-framework-sys",
2575+]
2576+
2577+[[package]]
2578+name = "security-framework"
25112579 version = "3.7.0"
25122580 source = "registry+https://github.com/rust-lang/crates.io-index"
25132581 checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
32233291 "log",
32243292 "once_cell",
32253293 "rustls",
3294+ "rustls-native-certs 0.7.3",
32263295 "rustls-pki-types",
32273296 "serde",
32283297 "serde_json",
+2−1
11 [workspace]
22 resolver = "3"
3−members = ["apps/api", "crates/*", "services/actions", "services/billing", "services/events", "services/identity", "services/integrations", "services/webhooks", "services/repos", "services/work"]
3+members = ["apps/api", "crates/*", "services/actions", "services/billing", "services/events", "services/identity", "services/integrations", "services/webhooks", "services/repos", "services/security", "services/work"]
44
55 [workspace.package]
66 edition = "2024"
1111 g1t-actions = { path = "crates/actions" }
1212 g1t-contracts = { path = "crates/contracts" }
1313 g1t-kit = { path = "crates/kit" }
14+g1t-scan = { path = "crates/scan" }
1415 g1t-secrets = { path = "crates/secrets" }
1516 serde = { version = "1", features = ["derive"] }
1617 serde_json = "1"
+259−0
1+//! Least privilege and the audit trail, around every operation.
2+//!
3+//! An agent's token is checked against its run's scope and the person it
4+//! acts for before anything runs (see `g1t_contracts::credentials`); a
5+//! runner's credential then acts downstream as that person. Everything an
6+//! agent does is recorded, reads included, as is every change a person or
7+//! a workspace token makes. Refusals are recorded with their rule.
8+
9+use g1t_contracts::audit::{AuditActor, AuditTarget, NewAuditEntry, RecordAuditArgs, Surface};
10+use g1t_contracts::credentials::{Decision, as_person, decide_operation, is_read};
11+use g1t_contracts::repos::RepoPath;
12+use g1t_contracts::{FailureCode, Outcome, PrincipalKind, User, Viewer};
13+use serde_json::Value;
14+use worker::{Request, Result, console_error};
15+
16+use crate::operations::{Op, Services};
17+
18+/// Where a request came in, and the id it is recorded under.
19+#[derive(Clone, Debug)]
20+pub struct AuditContext {
21+ pub request_id: String,
22+ pub surface: Surface,
23+}
24+
25+impl Default for AuditContext {
26+ fn default() -> Self {
27+ AuditContext {
28+ request_id: String::new(),
29+ surface: Surface::Rest,
30+ }
31+ }
32+}
33+
34+impl AuditContext {
35+ /// Cloudflare's ray id, which also finds the request in Workers logs.
36+ pub fn of(request: &Request, on_mcp: bool) -> Self {
37+ let ray = request.headers().get("cf-ray").ok().flatten();
38+ AuditContext {
39+ request_id: ray.unwrap_or_else(|| g1t_contracts::new_id("req", g1t_kit::now_ms())),
40+ surface: if on_mcp { Surface::Mcp } else { Surface::Rest },
41+ }
42+ }
43+}
44+
45+fn repo_path(input: &Value) -> Option<RepoPath> {
46+ let mut parts = input["repo"].as_str()?.split('/');
47+ match (parts.next(), parts.next(), parts.next()) {
48+ (Some(namespace), Some(name), None) if !namespace.is_empty() && !name.is_empty() => {
49+ Some(RepoPath {
50+ namespace: namespace.to_owned(),
51+ name: name.to_owned(),
52+ })
53+ }
54+ _ => None,
55+ }
56+}
57+
58+fn number(input: &Value) -> Option<u32> {
59+ match &input["number"] {
60+ Value::Number(number) => number.as_u64().and_then(|n| u32::try_from(n).ok()),
61+ Value::String(digits) => digits.parse().ok(),
62+ _ => None,
63+ }
64+}
65+
66+fn some_text(input: &Value, key: &str) -> Option<String> {
67+ input[key]
68+ .as_str()
69+ .filter(|value| !value.is_empty())
70+ .map(str::to_owned)
71+}
72+
73+/// What an operation named, for its entry.
74+pub fn target(user: &User, input: &Value) -> AuditTarget {
75+ let repo = repo_path(input);
76+ let workspace = repo
77+ .as_ref()
78+ .map(|repo| repo.namespace.clone())
79+ .or_else(|| some_text(input, "workspace"))
80+ .or_else(|| some_text(input, "slug"))
81+ .or_else(|| {
82+ user.acting
83+ .as_ref()
84+ .map(|acting| acting.scope.repo.namespace.clone())
85+ })
86+ .unwrap_or_default()
87+ .to_lowercase();
88+ AuditTarget {
89+ workspace,
90+ repo: repo.map(|repo| format!("{}/{}", repo.namespace, repo.name)),
91+ number: number(input),
92+ git_ref: some_text(input, "ref").or_else(|| some_text(input, "branch")),
93+ path: some_text(input, "path"),
94+ }
95+}
96+
97+/// The rule that let a person or a workspace token through: theirs is
98+/// decided by the service that owns what they asked about.
99+fn principal_rule(user: &User) -> &'static str {
100+ match user.kind {
101+ PrincipalKind::Workspace => "workspace-token",
102+ _ => "person",
103+ }
104+}
105+
106+/// Whether the API refused an agent before anything ran, and why.
107+pub fn decide(op: Op, services: &Services, viewer: &Viewer, input: &Value) -> Option<Decision> {
108+ let user = viewer
109+ .as_ref()
110+ .filter(|user| user.kind == PrincipalKind::Agent)?;
111+ let scope = services.scope.as_ref()?;
112+ Some(decide_operation(
113+ user,
114+ scope,
115+ op.name(),
116+ repo_path(input).as_ref(),
117+ op.needs_repo(),
118+ number(input),
119+ ))
120+}
121+
122+/// Runs `op` for `viewer`: enforcing an agent's scope first, and recording
123+/// what happened.
124+pub async fn run(
125+ op: Op,
126+ services: &Services,
127+ viewer: &Viewer,
128+ input: &Value,
129+) -> Result<Outcome<Value>> {
130+ let decision = decide(op, services, viewer, input);
131+ if let Some(decision) = decision.as_ref().filter(|decision| !decision.allowed) {
132+ record(op, services, viewer, input, decision, None).await;
133+ return Ok(Outcome::fail(
134+ FailureCode::Forbidden,
135+ decision.reason.as_deref().unwrap_or("Not allowed."),
136+ ));
137+ }
138+ // A runner's credential acts as the person, within the run's scope.
139+ let downstream: Viewer = viewer
140+ .as_ref()
141+ .and_then(as_person)
142+ .or_else(|| viewer.clone());
143+ let outcome = op.run(services, &downstream, input).await?;
144+ let decision = decision.unwrap_or_else(|| match viewer {
145+ Some(user) => Decision::allow(principal_rule(user)),
146+ None => Decision::allow("anonymous"),
147+ });
148+ record(op, services, viewer, input, &decision, Some(&outcome)).await;
149+ Ok(outcome)
150+}
151+
152+/// Appends the entry, if this is something the log keeps. A failure to
153+/// record is logged, never passed on to the caller.
154+async fn record(
155+ op: Op,
156+ services: &Services,
157+ viewer: &Viewer,
158+ input: &Value,
159+ decision: &Decision,
160+ outcome: Option<&Outcome<Value>>,
161+) {
162+ let Some(user) = viewer.as_ref() else {
163+ return;
164+ };
165+ let actor = AuditActor::of(user);
166+ if !actor.records_reads() && is_read(op.name()) {
167+ return;
168+ }
169+ let mut entry = NewAuditEntry::new(
170+ actor,
171+ op.name(),
172+ services.audit.surface,
173+ target(user, input),
174+ decision,
175+ services.audit.request_id.clone(),
176+ );
177+ match outcome {
178+ Some(Outcome::Ok(_)) => entry.result = Some("ok".to_owned()),
179+ Some(Outcome::Fail(failure)) => {
180+ let code = serde_json::to_value(failure.code)
181+ .ok()
182+ .and_then(|code| code.as_str().map(str::to_owned))
183+ .unwrap_or_else(|| "failed".to_owned());
184+ // Refused by the service that owns it: still a denial.
185+ if matches!(
186+ failure.code,
187+ FailureCode::Forbidden | FailureCode::Unauthenticated
188+ ) {
189+ entry.outcome = g1t_contracts::audit::AuditOutcome::Denied;
190+ entry.rule = "service".to_owned();
191+ }
192+ entry.message = Some(failure.message.clone());
193+ entry.result = Some(code);
194+ }
195+ None => entry.result = Some("forbidden".to_owned()),
196+ }
197+ let recorded: Result<u32> = g1t_kit::call(
198+ &services.events,
199+ "audit_record",
200+ &RecordAuditArgs {
201+ entries: vec![entry],
202+ },
203+ )
204+ .await;
205+ if let Err(error) = recorded {
206+ console_error!("audit entry not recorded for {}: {error}", op.name());
207+ }
208+}
209+
210+#[cfg(test)]
211+mod tests {
212+ use super::*;
213+ use g1t_contracts::credentials::{Acting, Principal};
214+ use g1t_contracts::identity::AgentScope;
215+ use serde_json::json;
216+
217+ #[test]
218+ fn the_target_comes_from_the_input() {
219+ let person = User {
220+ id: "usr_1".to_owned(),
221+ username: "syntaqx".to_owned(),
222+ ..User::default()
223+ };
224+ let target = target(
225+ &person,
226+ &json!({ "repo": "Acme/rocket", "number": "12", "path": "src/a.rs" }),
227+ );
228+ assert_eq!(target.workspace, "acme");
229+ assert_eq!(target.repo.as_deref(), Some("Acme/rocket"));
230+ assert_eq!(target.number, Some(12));
231+ assert_eq!(target.path.as_deref(), Some("src/a.rs"));
232+ assert_eq!(
233+ super::target(&person, &json!({ "workspace": "Ops" })).workspace,
234+ "ops"
235+ );
236+ }
237+
238+ #[test]
239+ fn an_agent_with_no_repository_is_logged_in_its_run_s_workspace() {
240+ let agent = User {
241+ kind: PrincipalKind::Agent,
242+ acting: Some(Box::new(Acting {
243+ credential_id: "tok_1".to_owned(),
244+ agent: "g1t-agent".to_owned(),
245+ on_behalf_of: Principal::default(),
246+ scope: AgentScope {
247+ repo: RepoPath {
248+ namespace: "acme".to_owned(),
249+ name: "rocket".to_owned(),
250+ },
251+ operations: vec![],
252+ run: None,
253+ },
254+ })),
255+ ..User::default()
256+ };
257+ assert_eq!(target(&agent, &json!({})).workspace, "acme");
258+ }
259+}
+5−5
9494 }
9595
9696 fn error(status: u16, message: &str) -> Result<Response> {
97− Ok(Response::from_json(&json!({ "error": { "message": message } }))?.with_status(status))
97+ Ok(crate::reply(&json!({ "error": { "message": message } }))?.with_status(status))
9898 }
9999
100100 fn valid_name(name: &str) -> bool {
161161 .into_iter()
162162 .map(|(_, meta)| json!({ "name": meta.name, "size": meta.size }))
163163 .collect();
164− Response::from_json(&listed)
164+ crate::reply(&listed)
165165 }
166166 (_, what) if what.starts_with("artifacts/") => {
167167 let name = decode(&what["artifacts/".len()..]);
175175 return error(413, "Artifacts are at most 60 MB.");
176176 }
177177 put(&kv, &base, &name, &bytes, ARTIFACT_TTL).await?;
178− return Response::from_json(&json!({ "name": name, "size": bytes.len() }));
178+ return crate::reply(&json!({ "name": name, "size": bytes.len() }));
179179 }
180180 match get(&kv, &base).await? {
181181 Some(bytes) => Response::from_bytes(bytes),
191191 let base = format!("c/{repo}/{key}");
192192 // A key is written once, as on GitHub.
193193 if kv.get(&base).text().await?.is_some() {
194− return Response::from_json(&json!({ "saved": false, "reason": "That key is already cached." }));
194+ return crate::reply(&json!({ "saved": false, "reason": "That key is already cached." }));
195195 }
196196 let bytes = request.bytes().await?;
197197 if bytes.len() > MAX_BYTES {
198198 return error(413, "Cache entries are at most 60 MB.");
199199 }
200200 put(&kv, &base, &key, &bytes, CACHE_TTL).await?;
201− return Response::from_json(&json!({ "saved": true }));
201+ return crate::reply(&json!({ "saved": true }));
202202 }
203203 // The exact key, else the newest entry under each restore key.
204204 let exact = format!("c/{repo}/{key}");
+71−27
44 //! operations (see [`operations::Op`]), which call the services that own
55 //! the data. This Worker holds none.
66
7+mod audit;
78 mod blobs;
89 mod mcp;
910 mod oauth;
1011 mod openapi;
1112 mod operations;
1213 mod renamed;
14+#[cfg(test)]
15+mod responses;
1316 mod rest;
1417
1518 use g1t_contracts::billing::FinishRunArgs;
2225 };
2326 use g1t_contracts::identity::AgentScope;
2427 use g1t_contracts::{Failure, FailureCode, Outcome, PrincipalKind, Viewer};
28+use g1t_kit::wire;
2529 use serde_json::{Value, json};
2630 use worker::{Context, Env, Method, Request, Response, Result, event};
2731
4246 }
4347 }
4448
49+/// A JSON response. Every body the API sends has its keys in `snake_case`;
50+/// the contracts it passes through are `camelCase`, so they are converted
51+/// here, on the way out (see [`g1t_kit::wire`]). The OpenAPI document and
52+/// the MCP protocol's own envelope keep the spelling their standards use.
53+pub(crate) fn reply<T: serde::Serialize>(value: &T) -> Result<Response> {
54+ Response::from_json(&wire::snake_case(serde_json::to_value(value)?))
55+}
56+
57+/// The parts of a job's spec (`POST /actions/jobs/{job}/spec`) that are the
58+/// workflow file, GitHub's contexts and event, and where to check out, all
59+/// passed through as they are.
60+const JOB_SPEC_AS_GIVEN: &[&str] = &[
61+ "spec", "workflow", "github", "event", "contexts", "checkout",
62+];
63+
4564 /// An error in the shape every endpoint uses.
4665 fn failure(failure: &Failure) -> Result<Response> {
47− Ok(Response::from_json(&json!({ "error": failure }))?.with_status(failure.code.http_status()))
66+ Ok(reply(&json!({ "error": failure }))?.with_status(failure.code.http_status()))
4867 }
4968
5069 fn fail(code: FailureCode, message: &str) -> Result<Response> {
138157 .collect();
139158 let body = request.text().await.unwrap_or_default();
140159 if body.len() > 1_000_000 {
141− return Ok(Response::from_json(&json!({ "message": "The body is too large." }))?.with_status(413));
160+ return Ok(reply(&json!({ "message": "The body is too large." }))?.with_status(413));
142161 }
143162 let received: g1t_contracts::integrations::Received = g1t_kit::call(
144163 &services.integrations,
146165 &json!({ "id": id, "headers": headers, "body": body }),
147166 )
148167 .await?;
149− Ok(Response::from_json(&json!({ "message": received.message }))?.with_status(received.status))
168+ Ok(reply(&json!({ "message": received.message }))?.with_status(received.status))
150169 }
151170
152171 async fn receive_stripe(request: &mut Request, env: &Env) -> Result<Response> {
153172 let signature = request.headers().get("stripe-signature")?.unwrap_or_default();
154173 let payload = request.text().await.unwrap_or_default();
155174 if payload.len() > 1_000_000 || signature.is_empty() {
156− return Ok(Response::from_json(&json!({ "message": "Not a Stripe event." }))?.with_status(400));
175+ return Ok(reply(&json!({ "message": "Not a Stripe event." }))?.with_status(400));
157176 }
158177 let handled: g1t_contracts::Outcome<bool> = g1t_kit::call(
159178 &env.service("BILLING")?,
164183 // A refusal is a 400, so Stripe shows it as failed; anything handled,
165184 // or already handled, is a 200, so Stripe stops sending it.
166185 Ok(match handled {
167− g1t_contracts::Outcome::Ok(_) => Response::from_json(&json!({ "received": true }))?,
186+ g1t_contracts::Outcome::Ok(_) => reply(&json!({ "received": true }))?,
168187 g1t_contracts::Outcome::Fail(failure) => {
169− Response::from_json(&json!({ "message": failure.message }))?.with_status(400)
188+ reply(&json!({ "message": failure.message }))?.with_status(400)
170189 }
171190 })
172191 }
181200 },
182201 )
183202 .await?;
184− Response::from_json(&json!({
203+ reply(&json!({
185204 "device_code": started.device_code,
186205 "user_code": started.user_code,
187206 "verification_uri": "https://g1t.sh/device",
201220 },
202221 )
203222 .await?;
204− Response::from_json(&match claim {
223+ reply(&match claim {
205224 DeviceClaim::Approved { token, user } => json!({
206225 "status": "approved",
207226 "token": token,
236255 )
237256 .await?;
238257 match reported {
239− Outcome::Ok(run) => Response::from_json(&json!({ "status": run.status })),
258+ Outcome::Ok(run) => reply(&json!({ "status": run.status })),
240259 Outcome::Fail(refused) => failure(&refused),
241260 }
242261 }
264283 )
265284 .await?;
266285 match reported {
267− Outcome::Ok(state) => Response::from_json(&json!({ "state": state })),
286+ Outcome::Ok(state) => reply(&json!({ "state": state })),
268287 Outcome::Fail(refused) => failure(&refused),
269288 }
270289 }
289308 )
290309 .await?;
291310 match reported {
292− Outcome::Ok(state) => Response::from_json(&json!({ "mergeable": state })),
311+ Outcome::Ok(state) => reply(&json!({ "mergeable": state })),
293312 Outcome::Fail(refused) => failure(&refused),
294313 }
295314 }
317336 )
318337 .await?;
319338 match reported {
320− Outcome::Ok(_) => Response::from_json(&json!({ "recorded": true })),
339+ Outcome::Ok(_) => reply(&json!({ "recorded": true })),
321340 Outcome::Fail(refused) => failure(&refused),
322341 }
323342 }
343362 )
344363 .await?;
345364 match reported {
346− Outcome::Ok(_) => Response::from_json(&json!({ "recorded": true })),
365+ Outcome::Ok(_) => reply(&json!({ "recorded": true })),
347366 Outcome::Fail(refused) => failure(&refused),
348367 }
349368 }
369388 )
370389 .await?;
371390 match charged {
372− Outcome::Ok(_) => Response::from_json(&json!({ "recorded": true })),
391+ Outcome::Ok(_) => reply(&json!({ "recorded": true })),
373392 Outcome::Fail(refused) => failure(&refused),
374393 }
375394 }
425444 // A fresh response: a fetched one's headers cannot be changed, and
426445 // every response gets the API's own on the way out.
427446 let status = answer.status_code();
428− return Ok(Response::from_bytes(answer.bytes().await?)?
447+ let bytes = answer.bytes().await?;
448+ let bytes = match serde_json::from_slice::<Value>(&bytes) {
449+ Ok(body) => serde_json::to_vec(&wire::snake_case(body))?,
450+ Err(_) => bytes,
451+ };
452+ return Ok(Response::from_bytes(bytes)?
429453 .with_status(status)
430454 .with_headers({
431455 let headers = worker::Headers::new();
448472 Ok(viewer) => viewer,
449473 Err(refused) => return Ok(refused),
450474 };
451− // An agent's token: what it may do comes with it.
452− if viewer.as_ref().is_some_and(|viewer| viewer.kind == PrincipalKind::Agent) {
475+ services.audit = audit::AuditContext::of(&request, on_mcp);
476+ // An agent's token: what it may do comes with it, on the composite
477+ // identity identity resolved it to.
478+ if let Some(acting) = viewer.as_ref().and_then(|viewer| viewer.acting.as_ref()) {
479+ services.scope = Some(acting.scope.clone());
480+ } else if viewer.as_ref().is_some_and(|viewer| viewer.kind == PrincipalKind::Agent) {
453481 let header = request.headers().get("authorization")?.unwrap_or_default();
454482 let token = header.split_once(' ').map(|(_, token)| token.trim()).unwrap_or_default();
455483 let scope: Option<AgentScope> = g1t_kit::call(
474502 }
475503
476504 match (method, path.trim_end_matches('/')) {
477− ("GET", "") => return Response::from_json(&index()),
505+ ("GET", "") => return reply(&index()),
478506 ("GET", "/openapi.json") => return Response::from_json(&openapi::document()),
479507 // A run's artifacts: listed, or one downloaded.
480508 ("GET", path) if path.starts_with("/repos/") && path.contains("/actions/runs/") && path.contains("/artifacts") => {
489517 )
490518 .await?;
491519 match seen {
492− Outcome::Ok(_) => Response::from_json(&blobs::of_run(env, run).await?),
520+ Outcome::Ok(_) => reply(&blobs::of_run(env, run).await?),
493521 Outcome::Fail(refused) => failure(&refused),
494522 }
495523 }
509537 )
510538 .await?;
511539 return match located {
512− Outcome::Ok(value) => Response::from_json(&value),
540+ Outcome::Ok(value) => reply(&value),
513541 Outcome::Fail(refused) => failure(&refused),
514542 };
515543 }
537565 )
538566 .await?;
539567 return match answered {
540− Outcome::Ok(value) => Response::from_json(&value),
568+ // A job's spec is the workflow and its contexts as GitHub
569+ // has them; only g1t's own keys around them are converted.
570+ Outcome::Ok(value) => Response::from_json(&wire::snake_case_keeping(value, JOB_SPEC_AS_GIVEN)),
541571 Outcome::Fail(refused) => failure(&refused),
542572 };
543573 }
552582 body["runId"] = json!(run_id);
553583 let reported: Outcome<Value> = g1t_kit::call(&services.work, "report_run", &body).await?;
554584 return match reported {
555− Outcome::Ok(status) => Response::from_json(&json!({ "status": status })),
585+ Outcome::Ok(status) => reply(&json!({ "status": status })),
556586 Outcome::Fail(refused) => failure(&refused),
557587 };
558588 }
589+ // What a run's agent learned, as memory candidates; the same token.
590+ ("POST", path) if path.starts_with("/agent-runs/") && path.ends_with("/learned") => {
591+ let run_id = path.trim_start_matches("/agent-runs/").trim_end_matches("/learned");
592+ let body = json_body(&mut request).await;
593+ let learned = json!({
594+ "runId": run_id,
595+ "token": body["token"].as_str().unwrap_or_default(),
596+ "items": body["items"].as_array().cloned().unwrap_or_default(),
597+ });
598+ let captured: Outcome<Value> = g1t_kit::call(&services.work, "report_learned", &learned).await?;
599+ return match captured {
600+ Outcome::Ok(captured) => reply(&captured),
601+ Outcome::Fail(refused) => failure(&refused),
602+ };
603+ }
559604 ("POST", path) if path.starts_with("/checks/") => {
560605 let run_id = path.trim_start_matches("/checks/").to_owned();
561606 return report_checks(&mut request, &services, &run_id).await;
590635 let Some((route, input)) = rest::resolve(method, &path, &query, body) else {
591636 return fail(FailureCode::NotFound, "No such endpoint.");
592637 };
593− match route.op.run(&services, &viewer, &input).await? {
594− Outcome::Ok(value) => Response::from_json(&value),
638+ match audit::run(route.op, &services, &viewer, &input).await? {
639+ Outcome::Ok(value) => reply(&value),
595640 Outcome::Fail(refused) => failure(&refused),
596641 }
597642 }
598643
599−/// Request bodies take the same keys as the MCP tools, `snake_case`; the
600−/// `camelCase` that responses use is accepted too, so a client can send
601−/// back what it read.
644+/// Request bodies take the same keys as the MCP tools, `snake_case`, as
645+/// responses use; the `camelCase` spelling is accepted too.
602646 fn snake_case_keys(body: Value) -> Value {
603647 let Value::Object(fields) = body else {
604648 return body;
+7−2
6868 let Some(op) = Op::by_name(params["name"].as_str().unwrap_or_default()) else {
6969 return Ok(Some(error(id, -32602, "Unknown tool.")));
7070 };
71− let outcome = op.run(services, viewer, &params["arguments"]).await?;
71+ let outcome = crate::audit::run(op, services, viewer, &params["arguments"]).await?;
7272 // A failed operation is a tool result the model can read and
7373 // act on, not a protocol error.
7474 let (text, failed) = match outcome {
75− Outcome::Ok(value) => (serde_json::to_string_pretty(&value)?, false),
75+ // In `snake_case`, as the REST API answers; the protocol's
76+ // own envelope keeps MCP's spelling.
77+ Outcome::Ok(value) => (
78+ serde_json::to_string_pretty(&g1t_kit::wire::snake_case(value))?,
79+ false,
80+ ),
7681 Outcome::Fail(failure) => (failure.message, true),
7782 };
7883 result(
+5−5
8383
8484 fn oauth_error(error: &str, description: &str) -> Result<Response> {
8585 let mut response =
86− Response::from_json(&json!({ "error": error, "error_description": description }))?
86+ crate::reply(&json!({ "error": error, "error_description": description }))?
8787 .with_status(400);
8888 response.headers_mut().set("cache-control", "no-store")?;
8989 Ok(response)
144144 "Give one to five redirect_uris: https addresses, http on localhost, or the application's own scheme.",
145145 );
146146 };
147− Ok(Response::from_json(&json!({
147+ Ok(crate::reply(&json!({
148148 "client_id": client_id,
149149 "client_name": client_name,
150150 "redirect_uris": redirect_uris,
214214 Outcome::Ok(tokens) => tokens,
215215 Outcome::Fail(failure) => return oauth_error("invalid_grant", &failure.message),
216216 };
217− let mut response = Response::from_json(&json!({
217+ let mut response = crate::reply(&json!({
218218 "access_token": tokens.access_token,
219219 "token_type": "Bearer",
220220 "expires_in": tokens.expires_in,
234234 ) -> Result<Option<Response>> {
235235 let response = match (method, path) {
236236 ("GET", "/.well-known/oauth-authorization-server") => {
237− Response::from_json(&server_metadata())?
237+ crate::reply(&server_metadata())?
238238 }
239239 // Asked for with or without the MCP server's path appended.
240240 ("GET", path) if path.starts_with("/.well-known/oauth-protected-resource") => {
241− Response::from_json(&json!({
241+ crate::reply(&json!({
242242 "resource": MCP_RESOURCE,
243243 "authorization_servers": [ISSUER],
244244 "bearer_methods_supported": ["header"],
+21−1
8484 &[Op::Remember, Op::Recall],
8585 ),
8686 (
87+ "Context",
88+ "A workspace's context hub: a catalog of what it builds and runs, built from its repositories, deployments and integrations, and one search across the catalog, docs, issues, pull requests and memory.",
89+ &[Op::SearchContext, Op::GetEntity],
90+ ),
91+ (
8792 "Actions",
8893 "GitHub Actions workflows in .g1t/workflows, their runs, and their jobs' logs.",
8994 &[
163168 Op::TakeMessages => "Take new messages",
164169 Op::Remember => "Remember something",
165170 Op::Recall => "Recall memory",
171+ Op::SearchContext => "Search the context hub",
172+ Op::GetEntity => "Get a catalog entry",
166173 Op::ListIssues => "List issues",
167174 Op::GetIssue => "Get an issue",
168175 Op::CreateIssue => "Create an issue",
555562 "info": {
556563 "title": "g1t API",
557564 "version": "1",
558− "description": "The REST API for g1t, a git forge built for agents. The same operations are available to agents as MCP tools at https://mcp.g1t.sh.",
565+ "description": "The REST API for g1t, a git forge built for agents. The same operations are available to agents as MCP tools at https://mcp.g1t.sh. Every name in a request or response body is `snake_case`; names you chose, such as a workflow's inputs or a secret's name, are returned as you wrote them.",
559566 "license": { "name": "MIT", "identifier": "MIT" },
560567 },
561568 "servers": [{ "url": "https://api.g1t.sh" }],
696703 );
697704 }
698705
706+ /// The reference shows responses as they are sent: `snake_case`.
707+ #[test]
708+ fn example_responses_are_snake_case() {
709+ let document = document();
710+ for (path, methods) in document["paths"].as_object().unwrap() {
711+ for (method, operation) in methods.as_object().unwrap() {
712+ let example = &operation["responses"]["200"]["content"]["application/json"]["example"];
713+ let leaked = g1t_kit::wire::camel_case_keys(example);
714+ assert!(leaked.is_empty(), "{method} {path} shows {leaked:?}");
715+ }
716+ }
717+ }
718+
699719 /// Examples never hold anything that reads as a real credential, which
700720 /// secret scanners rightly flag in a public repository: they end in `…`
701721 /// after the prefix, as `whsec_…` and `g1t_…` do.
+96−2
2626 pub integrations: Fetcher,
2727 pub webhooks: Fetcher,
2828 pub actions: Fetcher,
29+ /// The context hub: catalog and search.
30+ pub context: Fetcher,
31+ /// Where the request came in, for its audit entries.
32+ pub audit: crate::audit::AuditContext,
2933 /// Set for a request made with an agent's token: all it may do.
3034 pub scope: Option<AgentScope>,
3135 }
4246 integrations: env.service("INTEGRATIONS")?,
4347 webhooks: env.service("WEBHOOKS")?,
4448 actions: env.service("ACTIONS")?,
49+ context: env.service("CONTEXT")?,
4550 scope: None,
51+ audit: crate::audit::AuditContext::default(),
4652 })
4753 }
4854 }
6369 TakeMessages,
6470 Remember,
6571 Recall,
72+ SearchContext,
73+ GetEntity,
6674 ListIssues,
6775 GetIssue,
6876 CreateIssue,
287295 }
288296
289297 impl Op {
290− pub const ALL: [Op; 66] = [
298+ pub const ALL: [Op; 68] = [
291299 Op::Whoami,
292300 Op::CreateWorkspace,
293301 Op::ListRepos,
302310 Op::TakeMessages,
303311 Op::Remember,
304312 Op::Recall,
313+ Op::SearchContext,
314+ Op::GetEntity,
305315 Op::ListIssues,
306316 Op::GetIssue,
307317 Op::CreateIssue,
376386 Op::TakeMessages => "take_messages",
377387 Op::Remember => "remember",
378388 Op::Recall => "recall",
389+ Op::SearchContext => "search_context",
390+ Op::GetEntity => "get_entity",
379391 Op::UpdateRepoSettings => "update_repo_settings",
380392 Op::ListIssues => "list_issues",
381393 Op::GetIssue => "get_issue",
463475 Op::Recall => {
464476 "Search what the project and its workspace remember, by words in any order, or list it all without a query. Pinned memories come first, then the most recently used. Members of the workspace and g1t's agents only."
465477 }
478+ Op::SearchContext => {
479+ "One search across a workspace's context hub: its catalog (projects, apps, APIs, packages, languages, owners, environments, integrations, docs), the text of its docs, its issues and pull requests, and, for members and g1t's agents, its kept memory. Results are ranked by meaning, each labelled with its kind, where it came from, who wrote it and how fresh it is; matching words answers when meaning cannot. Give the workspace, or a repository in it. Narrow with project (a project's slug) and kinds. Reads only what you may see: memory and private projects are for members."
480+ }
481+ Op::GetEntity => {
482+ "One entry of a workspace's catalog, by kind and its id or key (a project's slug, a package as npm:<name>, an owner's username), with every relation it has: what it depends on, who owns it, where it deploys, what documents it, what it exposes and uses. search_context finds entries."
483+ }
466484 Op::TakeMessages => {
467485 "For a g1t agent at work: the messages sent to it that it has not seen yet, from people and from other agents. Each is returned once."
468486 }
697715 }),
698716 &["repo"],
699717 ),
718+ Op::SearchContext => object(
719+ json!({
720+ "query": { "type": "string", "description": "What you want to know, in words: \"how do we deploy the api\", \"who owns billing\"." },
721+ "workspace": workspace_schema(),
722+ "repo": { "type": "string", "description": "Instead of workspace: a repository in it, as \"owner/name\"." },
723+ "project": { "type": "string", "description": "Only what is about this project, by its slug." },
724+ "kinds": {
725+ "type": "array",
726+ "items": {
727+ "type": "string",
728+ "enum": ["project", "app", "api", "package", "language", "owner", "environment", "integration", "doc", "memory", "issue", "pull"],
729+ },
730+ "description": "Only these kinds. All of them if not given.",
731+ },
732+ "limit": { "type": "integer", "description": "At most 50; 20 if not given." },
733+ }),
734+ &["query"],
735+ ),
736+ Op::GetEntity => object(
737+ json!({
738+ "kind": {
739+ "type": "string",
740+ "enum": ["project", "app", "api", "package", "language", "owner", "environment", "integration", "doc"],
741+ },
742+ "id": { "type": "string", "description": "Its id (ent_…), or its key: a project's slug, npm:<name>, a username." },
743+ "workspace": workspace_schema(),
744+ "repo": { "type": "string", "description": "Instead of workspace: a repository in it, as \"owner/name\"." },
745+ }),
746+ &["kind", "id"],
747+ ),
700748 Op::UpdateRepoSettings => object(
701749 json!({
702750 "repo": repo_schema(),
11561204 }
11571205
11581206 /// Whether the operation is about one repository, named by `repo`.
1159− fn needs_repo(self) -> bool {
1207+ pub(crate) fn needs_repo(self) -> bool {
11601208 !matches!(
11611209 self,
11621210 Op::Whoami
11631211 | Op::CreateWorkspace
1212+ | Op::SearchContext
1213+ | Op::GetEntity
11641214 | Op::ListRepos
11651215 | Op::CreateRepo
11661216 | Op::ListIntegrations
14011451 )
14021452 .await
14031453 }
1454+ Op::SearchContext | Op::GetEntity => {
1455+ // The workspace named, or the repository's, or an agent's own.
1456+ let workspace = match optional_text(input, "workspace") {
1457+ Some(workspace) => workspace.to_lowercase(),
1458+ None if !repo.namespace.is_empty() => repo.namespace.to_lowercase(),
1459+ None => match &services.scope {
1460+ Some(scope) => scope.repo.namespace.to_lowercase(),
1461+ None => return failed(FailureCode::Invalid, "Give the workspace, or a repository in it as \"owner/name\"."),
1462+ },
1463+ };
1464+ if let Some(scope) = &services.scope
1465+ && !scope.repo.namespace.eq_ignore_ascii_case(&workspace)
1466+ {
1467+ return failed(
1468+ FailureCode::Forbidden,
1469+ &format!("A g1t agent's token works in the {} workspace only.", scope.repo.namespace),
1470+ );
1471+ }
1472+ if self == Op::SearchContext {
1473+ pass(
1474+ &services.context,
1475+ "search",
1476+ &json!({
1477+ "workspace": workspace,
1478+ "viewer": viewer,
1479+ "query": text(input, "query"),
1480+ "project": optional_text(input, "project"),
1481+ // A list, or in a URL, comma-separated.
1482+ "kinds": strings(input, "kinds").or_else(|| {
1483+ optional_text(input, "kinds").map(|kinds| kinds.split(',').map(|kind| kind.trim().to_owned()).collect())
1484+ }),
1485+ "limit": integer(input, "limit"),
1486+ }),
1487+ )
1488+ .await
1489+ } else {
1490+ pass(
1491+ &services.context,
1492+ "entity",
1493+ &json!({ "workspace": workspace, "viewer": viewer, "kind": text(input, "kind"), "id": text(input, "id") }),
1494+ )
1495+ .await
1496+ }
1497+ }
14041498 Op::Recall => {
14051499 pass(
14061500 work,
+594−465
5252 "slug": "syntaqx-labs",
5353 "name": "Syntaqx Labs",
5454 "description": null,
55− "createdAt": "2026-10-04T16:02:51.337Z",
56− "memberCount": 1
55+ "created_at": "2026-10-04T16:02:51.337Z",
56+ "member_count": 1
5757 }
5858 },
5959 "list_repos": {
6666 "namespace": "syntaqx",
6767 "name": "hello",
6868 "description": "A tiny service that says hello.",
69− "isPrivate": false,
70− "ownerId": "usr_01kkntcg1eeb98j62xjm7eh09p",
71− "defaultBranch": "main",
72− "forkOf": null,
69+ "is_private": false,
70+ "owner_id": "usr_01kkntcg1eeb98j62xjm7eh09p",
71+ "default_branch": "main",
72+ "fork_of": null,
7373 "protected": true,
74− "createdAt": "2026-09-28T14:11:52.640Z"
74+ "created_at": "2026-09-28T14:11:52.640Z"
7575 }
7676 ],
7777 "notes": "Returns at most 50 repositories, newest first. Forks made for pull requests are left out."
8888 "namespace": "syntaqx",
8989 "name": "hello-cli",
9090 "description": "Command-line client for hello.",
91− "isPrivate": false,
92− "ownerId": "usr_01kkntcg1eeb98j62xjm7eh09p",
93− "defaultBranch": "main",
94− "forkOf": null,
91+ "is_private": false,
92+ "owner_id": "usr_01kkntcg1eeb98j62xjm7eh09p",
93+ "default_branch": "main",
94+ "fork_of": null,
9595 "protected": false,
96− "createdAt": "2026-10-04T16:05:12.913Z"
96+ "created_at": "2026-10-04T16:05:12.913Z"
9797 },
98− "notes": "`ownerId` is the id of the person who created the repository. With `import_url`, `defaultBranch` is the default branch of the repository copied."
98+ "notes": "`owner_id` is the id of the person who created the repository. With `import_url`, `default_branch` is the default branch of the repository copied."
9999 },
100100 "get_repo": {
101101 "response": {
103103 "namespace": "syntaqx",
104104 "name": "hello",
105105 "description": "A tiny service that says hello.",
106− "isPrivate": false,
107− "ownerId": "usr_01kkntcg1eeb98j62xjm7eh09p",
108− "defaultBranch": "main",
109− "forkOf": null,
106+ "is_private": false,
107+ "owner_id": "usr_01kkntcg1eeb98j62xjm7eh09p",
108+ "default_branch": "main",
109+ "fork_of": null,
110110 "protected": true,
111− "createdAt": "2026-09-28T14:11:52.640Z"
111+ "created_at": "2026-09-28T14:11:52.640Z"
112112 }
113113 },
114114 "update_repo": {
121121 "namespace": "syntaqx",
122122 "name": "hello",
123123 "description": "Says hello, politely.",
124− "isPrivate": false,
125− "ownerId": "usr_01kkntcg1eeb98j62xjm7eh09p",
126− "defaultBranch": "main",
127− "forkOf": null,
124+ "is_private": false,
125+ "owner_id": "usr_01kkntcg1eeb98j62xjm7eh09p",
126+ "default_branch": "main",
127+ "fork_of": null,
128128 "protected": true,
129− "createdAt": "2026-09-28T14:11:52.640Z"
129+ "created_at": "2026-09-28T14:11:52.640Z"
130130 }
131131 },
132132 "get_repo_settings": {
133133 "response": {
134− "autoMerge": false,
135− "requireUpToDate": false,
136− "requiredApprovals": 0,
137− "countAgentApprovals": true,
138− "allowIgnoringChecks": true,
139− "agentReview": true,
140− "maxRevisions": 2,
141− "mergeQueue": false,
142− "updatedBy": null,
143− "updatedAt": null
134+ "auto_merge": false,
135+ "require_up_to_date": false,
136+ "required_approvals": 0,
137+ "count_agent_approvals": true,
138+ "allow_ignoring_checks": true,
139+ "agent_review": true,
140+ "max_revisions": 2,
141+ "merge_queue": false,
142+ "updated_by": null,
143+ "updated_at": null
144144 }
145145 },
146146 "update_repo_settings": {
150150 "merge_queue": true
151151 },
152152 "response": {
153− "autoMerge": false,
154− "requireUpToDate": false,
155− "requiredApprovals": 1,
156− "countAgentApprovals": false,
157− "allowIgnoringChecks": true,
158− "agentReview": true,
159− "maxRevisions": 2,
160− "mergeQueue": true,
161− "updatedBy": "syntaqx",
162− "updatedAt": "2026-10-04T16:20:37.508Z"
153+ "auto_merge": false,
154+ "require_up_to_date": false,
155+ "required_approvals": 1,
156+ "count_agent_approvals": false,
157+ "allow_ignoring_checks": true,
158+ "agent_review": true,
159+ "max_revisions": 2,
160+ "merge_queue": true,
161+ "updated_by": "syntaqx",
162+ "updated_at": "2026-10-04T16:20:37.508Z"
163163 },
164164 "notes": "`required_approvals` is at most 6 and `max_revisions` at most 5. See [what a repository can ask for](/guides/g1t-agents/#what-a-repository-can-ask-for)."
165165 },
173173 "type": "pull.opened",
174174 "source": "work",
175175 "time": "2026-10-01T18:20:02.117Z",
176− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
176+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
177177 "actor": "usr_01kkntcg1eeb98j62xjm7eh09p",
178178 "data": {
179− "pullId": "pr_01m43smh3vexsr5pmp60qwv0vs",
180− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
179+ "pull_id": "pr_01m43smh3vexsr5pmp60qwv0vs",
180+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
181181 "number": 14,
182182 "issue": 12,
183183 "agent": "claude-code"
188188 "type": "issue.opened",
189189 "source": "work",
190190 "time": "2026-10-01T18:04:11.482Z",
191− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
191+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
192192 "actor": "usr_01kkntcg1eeb98j62xjm7eh09p",
193193 "data": {
194− "issueId": "iss_01m43shrzpfe49x74ga7sj1c6v",
195− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
194+ "issue_id": "iss_01m43shrzpfe49x74ga7sj1c6v",
195+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
196196 "number": 12,
197197 "title": "Greeting should name the caller"
198198 }
208208 "response": [
209209 {
210210 "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
211− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
211+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
212212 "number": 12,
213213 "title": "Greeting should name the caller",
214214 "body": "Take a name from the first argument; fall back to world.",
220220 ],
221221 "state": "open",
222222 "reason": null,
223− "resolvedBy": null,
223+ "resolved_by": null,
224224 "author": {
225225 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
226226 "username": "syntaqx",
228228 "verified": false,
229229 "workspaces": []
230230 },
231− "createdAt": "2026-10-01T18:04:11.482Z",
232− "updatedAt": "2026-10-01T18:04:11.482Z",
233− "closedAt": null,
234− "pullCount": 1,
235− "commentCount": 0,
231+ "created_at": "2026-10-01T18:04:11.482Z",
232+ "updated_at": "2026-10-01T18:04:11.482Z",
233+ "closed_at": null,
234+ "pull_count": 1,
235+ "comment_count": 0,
236236 "assignees": [],
237− "blockedBy": [],
237+ "blocked_by": [],
238238 "queued": false,
239239 "agent": "claude-code"
240240 }
241241 ],
242− "notes": "Returns at most 100 issues, newest first. `commentCount` counts comments, not events such as \"opened #14 for this\"."
242+ "notes": "Returns at most 100 issues, newest first. `comment_count` counts comments, not events such as \"opened #14 for this\"."
243243 },
244244 "create_issue": {
245245 "request": {
254254 },
255255 "response": {
256256 "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
257− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
257+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
258258 "number": 12,
259259 "title": "Greeting should name the caller",
260260 "body": "Take a name from the first argument; fall back to world.",
266266 ],
267267 "state": "open",
268268 "reason": null,
269− "resolvedBy": null,
269+ "resolved_by": null,
270270 "author": {
271271 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
272272 "username": "syntaqx",
274274 "verified": false,
275275 "workspaces": []
276276 },
277− "createdAt": "2026-10-01T18:04:11.482Z",
278− "updatedAt": "2026-10-01T18:04:11.482Z",
279− "closedAt": null,
280− "pullCount": 0,
281− "commentCount": 0,
277+ "created_at": "2026-10-01T18:04:11.482Z",
278+ "updated_at": "2026-10-01T18:04:11.482Z",
279+ "closed_at": null,
280+ "pull_count": 0,
281+ "comment_count": 0,
282282 "assignees": [],
283− "blockedBy": [],
283+ "blocked_by": [],
284284 "queued": false,
285285 "agent": null
286286 },
290290 "response": {
291291 "issue": {
292292 "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
293− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
293+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
294294 "number": 12,
295295 "title": "Greeting should name the caller",
296296 "body": "Take a name from the first argument; fall back to world.",
302302 ],
303303 "state": "closed",
304304 "reason": "completed",
305− "resolvedBy": 14,
305+ "resolved_by": 14,
306306 "author": {
307307 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
308308 "username": "syntaqx",
310310 "verified": false,
311311 "workspaces": []
312312 },
313− "createdAt": "2026-10-01T18:04:11.482Z",
314− "updatedAt": "2026-10-01T18:52:17.093Z",
315− "closedAt": "2026-10-01T18:52:17.093Z",
316− "pullCount": 2,
317− "commentCount": 0,
313+ "created_at": "2026-10-01T18:04:11.482Z",
314+ "updated_at": "2026-10-01T18:52:17.093Z",
315+ "closed_at": "2026-10-01T18:52:17.093Z",
316+ "pull_count": 2,
317+ "comment_count": 0,
318318 "assignees": [],
319− "blockedBy": [],
319+ "blocked_by": [],
320320 "queued": false,
321321 "agent": null
322322 },
323323 "pulls": [
324324 {
325325 "id": "pr_01m43sjq8tcz5rbm2a7k4d9e6w",
326− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
326+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
327327 "number": 13,
328328 "issue": 12,
329329 "title": "Greeting should name the caller",
335335 "namespace": "pulls",
336336 "name": "pr_01m43sjq8tcz5rbm2a7k4d9e6w"
337337 },
338− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
338+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
339339 "branch": null,
340− "headCommit": null,
341− "mergeBase": null,
342− "mergedBy": null,
343− "mergedAt": null,
344− "supersededBy": 14,
345− "checkStatus": null,
340+ "head_commit": null,
341+ "merge_base": null,
342+ "merged_by": null,
343+ "merged_at": null,
344+ "superseded_by": 14,
345+ "check_status": null,
346346 "files": [],
347347 "assignees": [],
348348 "reviewers": [],
353353 "verified": false,
354354 "workspaces": []
355355 },
356− "createdAt": "2026-10-01T18:20:02.117Z",
357− "updatedAt": "2026-10-01T18:20:02.117Z"
356+ "created_at": "2026-10-01T18:20:02.117Z",
357+ "updated_at": "2026-10-01T18:20:02.117Z"
358358 },
359359 {
360360 "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
361− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
361+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
362362 "number": 14,
363363 "issue": 12,
364364 "title": "Greeting should name the caller",
370370 "namespace": "pulls",
371371 "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
372372 },
373− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
373+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
374374 "branch": null,
375− "headCommit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
376− "mergeBase": "3a7e9c1b5d2f4a6c8e0b2d4f6a8c0e2b4d6f8a1c",
377− "mergedBy": "syntaqx",
378− "mergedAt": "2026-10-01T18:52:17.093Z",
379− "supersededBy": null,
380− "checkStatus": "passed",
375+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
376+ "merge_base": "3a7e9c1b5d2f4a6c8e0b2d4f6a8c0e2b4d6f8a1c",
377+ "merged_by": "syntaqx",
378+ "merged_at": "2026-10-01T18:52:17.093Z",
379+ "superseded_by": null,
380+ "check_status": "passed",
381381 "files": [
382382 {
383383 "path": "src/main.rs",
396396 "verified": false,
397397 "workspaces": []
398398 },
399− "createdAt": "2026-10-01T18:20:02.117Z",
400− "updatedAt": "2026-10-01T18:52:17.093Z"
399+ "created_at": "2026-10-01T18:20:02.117Z",
400+ "updated_at": "2026-10-01T18:52:17.093Z"
401401 }
402402 ],
403403 "comments": [
415415 "path": null,
416416 "line": null,
417417 "verdict": null,
418− "createdAt": "2026-10-01T18:20:02.141Z"
418+ "created_at": "2026-10-01T18:20:02.141Z"
419419 }
420420 ]
421421 },
422− "notes": "A closed issue says how it was closed: `reason` is `completed` or `not_planned`, and `resolvedBy` is the number of the pull request whose merge closed it. Each pull request made for it is listed; one closed because another was merged has `supersededBy` set."
422+ "notes": "A closed issue says how it was closed: `reason` is `completed` or `not_planned`, and `resolved_by` is the number of the pull request whose merge closed it. Each pull request made for it is listed; one closed because another was merged has `superseded_by` set."
423423 },
424424 "update_issue": {
425425 "request": {
433433 },
434434 "response": {
435435 "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
436− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
436+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
437437 "number": 12,
438438 "title": "Greeting should name the caller",
439439 "body": "Take a name from the first argument; fall back to world.",
446446 ],
447447 "state": "open",
448448 "reason": null,
449− "resolvedBy": null,
449+ "resolved_by": null,
450450 "author": {
451451 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
452452 "username": "syntaqx",
454454 "verified": false,
455455 "workspaces": []
456456 },
457− "createdAt": "2026-10-01T18:04:11.482Z",
458− "updatedAt": "2026-10-01T18:09:47.305Z",
459− "closedAt": null,
460− "pullCount": 0,
461− "commentCount": 0,
457+ "created_at": "2026-10-01T18:04:11.482Z",
458+ "updated_at": "2026-10-01T18:09:47.305Z",
459+ "closed_at": null,
460+ "pull_count": 0,
461+ "comment_count": 0,
462462 "assignees": [
463463 "syntaqx"
464464 ],
465− "blockedBy": [],
465+ "blocked_by": [],
466466 "queued": false,
467467 "agent": null
468468 }
473473 },
474474 "response": {
475475 "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
476− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
476+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
477477 "number": 12,
478478 "title": "Greeting should name the caller",
479479 "body": "Take a name from the first argument; fall back to world.",
485485 ],
486486 "state": "closed",
487487 "reason": "not_planned",
488− "resolvedBy": null,
488+ "resolved_by": null,
489489 "author": {
490490 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
491491 "username": "syntaqx",
493493 "verified": false,
494494 "workspaces": []
495495 },
496− "createdAt": "2026-10-01T18:04:11.482Z",
497− "updatedAt": "2026-10-01T19:30:00.214Z",
498− "closedAt": "2026-10-01T19:30:00.214Z",
499− "pullCount": 0,
500− "commentCount": 0,
496+ "created_at": "2026-10-01T18:04:11.482Z",
497+ "updated_at": "2026-10-01T19:30:00.214Z",
498+ "closed_at": "2026-10-01T19:30:00.214Z",
499+ "pull_count": 0,
500+ "comment_count": 0,
501501 "assignees": [],
502− "blockedBy": [],
502+ "blocked_by": [],
503503 "queued": false,
504504 "agent": null
505505 }
507507 "reopen_issue": {
508508 "response": {
509509 "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
510− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
510+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
511511 "number": 12,
512512 "title": "Greeting should name the caller",
513513 "body": "Take a name from the first argument; fall back to world.",
519519 ],
520520 "state": "open",
521521 "reason": null,
522− "resolvedBy": null,
522+ "resolved_by": null,
523523 "author": {
524524 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
525525 "username": "syntaqx",
527527 "verified": false,
528528 "workspaces": []
529529 },
530− "createdAt": "2026-10-01T18:04:11.482Z",
531− "updatedAt": "2026-10-01T19:41:52.830Z",
532− "closedAt": null,
533− "pullCount": 0,
534− "commentCount": 0,
530+ "created_at": "2026-10-01T18:04:11.482Z",
531+ "updated_at": "2026-10-01T19:41:52.830Z",
532+ "closed_at": null,
533+ "pull_count": 0,
534+ "comment_count": 0,
535535 "assignees": [],
536− "blockedBy": [],
536+ "blocked_by": [],
537537 "queued": false,
538538 "agent": null
539539 }
544544 },
545545 "response": {
546546 "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
547− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
547+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
548548 "number": 14,
549549 "issue": 12,
550550 "title": "Greeting should name the caller",
556556 "namespace": "pulls",
557557 "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
558558 },
559− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
559+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
560560 "branch": null,
561− "headCommit": null,
562− "mergeBase": null,
563− "mergedBy": null,
564− "mergedAt": null,
565− "supersededBy": null,
566− "checkStatus": null,
561+ "head_commit": null,
562+ "merge_base": null,
563+ "merged_by": null,
564+ "merged_at": null,
565+ "superseded_by": null,
566+ "check_status": null,
567567 "files": [],
568568 "assignees": [],
569569 "reviewers": [],
574574 "verified": false,
575575 "workspaces": []
576576 },
577− "createdAt": "2026-10-01T18:20:02.117Z",
578− "updatedAt": "2026-10-01T18:20:02.117Z"
577+ "created_at": "2026-10-01T18:20:02.117Z",
578+ "updated_at": "2026-10-01T18:20:02.117Z"
579579 },
580580 "notes": "The response is the pull request the g1t agent opened, still a draft. Follow it with [get a pull request](/reference/api/pull-requests/get-pull-request/): `lifecycle` says what the agent is doing."
581581 },
602602 "path": null,
603603 "line": null,
604604 "verdict": null,
605− "createdAt": "2026-10-01T18:12:30.551Z"
605+ "created_at": "2026-10-01T18:12:30.551Z"
606606 }
607607 },
608608 "list_labels": {
621621 "brief": "Greet people by name, from the command line and the web page."
622622 },
623623 "response": {
624− "planId": "pln_01m43s9c4e8g2j6m0q4t8x2b6d"
624+ "plan_id": "pln_01m43s9c4e8g2j6m0q4t8x2b6d"
625625 }
626626 },
627627 "get_plan": {
630630 },
631631 "response": {
632632 "id": "pln_01m43s9c4e8g2j6m0q4t8x2b6d",
633− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
633+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
634634 "brief": "Greet people by name, from the command line and the web page.",
635635 "status": "ready",
636636 "summary": "Name parsing first, then the two places that print the greeting.",
647647 "files": [
648648 "src/greet.rs"
649649 ],
650− "dependsOn": [],
650+ "depends_on": [],
651651 "number": null
652652 },
653653 {
662662 "files": [
663663 "src/web.rs"
664664 ],
665− "dependsOn": [
665+ "depends_on": [
666666 1
667667 ],
668668 "number": null
676676 "verified": false,
677677 "workspaces": []
678678 },
679− "createdAt": "2026-10-01T17:58:40.006Z",
680− "finishedAt": "2026-10-01T18:01:12.774Z",
679+ "created_at": "2026-10-01T17:58:40.006Z",
680+ "finished_at": "2026-10-01T18:01:12.774Z",
681681 "progress": [],
682682 "exchanges": []
683683 },
692692 },
693693 "response": {
694694 "id": "pln_01m43s9c4e8g2j6m0q4t8x2b6d",
695− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
695+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
696696 "brief": "Greet people by name, from the command line and the web page.",
697697 "status": "applied",
698698 "summary": "Name parsing first, then the two places that print the greeting.",
709709 "files": [
710710 "src/greet.rs"
711711 ],
712− "dependsOn": [],
712+ "depends_on": [],
713713 "number": 12
714714 },
715715 {
724724 "files": [
725725 "src/web.rs"
726726 ],
727− "dependsOn": [
727+ "depends_on": [
728728 1
729729 ],
730730 "number": 13
738738 "verified": false,
739739 "workspaces": []
740740 },
741− "createdAt": "2026-10-01T17:58:40.006Z",
742− "finishedAt": "2026-10-01T18:01:12.774Z",
741+ "created_at": "2026-10-01T17:58:40.006Z",
742+ "finished_at": "2026-10-01T18:01:12.774Z",
743743 "progress": [],
744744 "exchanges": []
745745 },
752752 "response": [
753753 {
754754 "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
755− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
755+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
756756 "number": 14,
757757 "issue": 12,
758758 "title": "Greeting should name the caller",
764764 "namespace": "pulls",
765765 "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
766766 },
767− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
767+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
768768 "branch": null,
769− "headCommit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
770− "mergeBase": null,
771− "mergedBy": null,
772− "mergedAt": null,
773− "supersededBy": null,
774− "checkStatus": "passed",
769+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
770+ "merge_base": null,
771+ "merged_by": null,
772+ "merged_at": null,
773+ "superseded_by": null,
774+ "check_status": "passed",
775775 "files": [
776776 {
777777 "path": "src/main.rs",
790790 "verified": false,
791791 "workspaces": []
792792 },
793− "createdAt": "2026-10-01T18:20:02.117Z",
794− "updatedAt": "2026-10-01T18:35:44.902Z"
793+ "created_at": "2026-10-01T18:20:02.117Z",
794+ "updated_at": "2026-10-01T18:35:44.902Z"
795795 }
796796 ],
797− "notes": "Returns at most 100 pull requests, newest first. `checkStatus` is `queued`, `running`, `passed`, `failed`, `errored`, or `null` when no checks have run against the head."
797+ "notes": "Returns at most 100 pull requests, newest first. `check_status` is `queued`, `running`, `passed`, `failed`, `errored`, or `null` when no checks have run against the head."
798798 },
799799 "create_pull_request": {
800800 "request": {
804804 "response": {
805805 "pull": {
806806 "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
807− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
807+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
808808 "number": 14,
809809 "issue": 12,
810810 "title": "Greeting should name the caller",
816816 "namespace": "pulls",
817817 "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
818818 },
819− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
819+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
820820 "branch": null,
821− "headCommit": null,
822− "mergeBase": null,
823− "mergedBy": null,
824− "mergedAt": null,
825− "supersededBy": null,
826− "checkStatus": null,
821+ "head_commit": null,
822+ "merge_base": null,
823+ "merged_by": null,
824+ "merged_at": null,
825+ "superseded_by": null,
826+ "check_status": null,
827827 "files": [],
828828 "assignees": [],
829829 "reviewers": [],
834834 "verified": false,
835835 "workspaces": []
836836 },
837− "createdAt": "2026-10-01T18:20:02.117Z",
838− "updatedAt": "2026-10-01T18:20:02.117Z"
837+ "created_at": "2026-10-01T18:20:02.117Z",
838+ "updated_at": "2026-10-01T18:20:02.117Z"
839839 },
840840 "git": {
841841 "remote": "https://g1t.sh/pulls/pr_01m43smh3vexsr5pmp60qwv0vs.git",
849849 "response": {
850850 "pull": {
851851 "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
852− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
852+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
853853 "number": 14,
854854 "issue": 12,
855855 "title": "Greeting should name the caller",
861861 "namespace": "pulls",
862862 "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
863863 },
864− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
864+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
865865 "branch": null,
866− "headCommit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
867− "mergeBase": null,
868− "mergedBy": null,
869− "mergedAt": null,
870− "supersededBy": null,
871− "checkStatus": "passed",
866+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
867+ "merge_base": null,
868+ "merged_by": null,
869+ "merged_at": null,
870+ "superseded_by": null,
871+ "check_status": "passed",
872872 "files": [
873873 {
874874 "path": "src/main.rs",
887887 "verified": false,
888888 "workspaces": []
889889 },
890− "createdAt": "2026-10-01T18:20:02.117Z",
891− "updatedAt": "2026-10-01T18:35:44.902Z"
890+ "created_at": "2026-10-01T18:20:02.117Z",
891+ "updated_at": "2026-10-01T18:35:44.902Z"
892892 },
893893 "issue": {
894894 "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
895− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
895+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
896896 "number": 12,
897897 "title": "Greeting should name the caller",
898898 "body": "Take a name from the first argument; fall back to world.",
904904 ],
905905 "state": "open",
906906 "reason": null,
907− "resolvedBy": null,
907+ "resolved_by": null,
908908 "author": {
909909 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
910910 "username": "syntaqx",
912912 "verified": false,
913913 "workspaces": []
914914 },
915− "createdAt": "2026-10-01T18:04:11.482Z",
916− "updatedAt": "2026-10-01T18:20:02.117Z",
917− "closedAt": null,
918− "pullCount": 1,
919− "commentCount": 0,
915+ "created_at": "2026-10-01T18:04:11.482Z",
916+ "updated_at": "2026-10-01T18:20:02.117Z",
917+ "closed_at": null,
918+ "pull_count": 1,
919+ "comment_count": 0,
920920 "assignees": [],
921− "blockedBy": [],
921+ "blocked_by": [],
922922 "queued": false,
923923 "agent": "claude-code"
924924 },
937937 "path": null,
938938 "line": null,
939939 "verdict": null,
940− "createdAt": "2026-10-01T18:33:10.420Z"
940+ "created_at": "2026-10-01T18:33:10.420Z"
941941 }
942942 ],
943943 "checks": {
944944 "id": "chk_01m43sw8e2g6j0m4q8t2x6a0c4",
945− "headCommit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
945+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
946946 "status": "passed",
947947 "results": [
948948 {
949949 "command": "cargo test",
950950 "passed": true,
951− "exitCode": 0,
951+ "exit_code": 0,
952952 "output": "test result: ok. 3 passed; 0 failed",
953− "durationMs": 18234
953+ "duration_ms": 18234
954954 }
955955 ],
956956 "error": null,
957− "createdAt": "2026-10-01T18:33:11.002Z",
958− "finishedAt": "2026-10-01T18:35:44.902Z"
957+ "created_at": "2026-10-01T18:33:11.002Z",
958+ "finished_at": "2026-10-01T18:35:44.902Z"
959959 },
960960 "overlaps": [],
961961 "behind": false,
968968 "id": "msg_01m43tx5egeh4t3f9zcnjenqvz",
969969 "author": "syntaqx",
970970 "body": "Keep \"world\" as the default when no name is given.",
971− "createdAt": "2026-10-01T18:25:00.310Z",
972− "deliveredAt": "2026-10-01T18:25:31.007Z",
971+ "created_at": "2026-10-01T18:25:00.310Z",
972+ "delivered_at": "2026-10-01T18:25:31.007Z",
973973 "kind": "message",
974− "fromNumber": null,
975− "toNumber": 14,
974+ "from_number": null,
975+ "to_number": 14,
976976 "answer": null,
977977 "declined": false
978978 }
10341034 },
10351035 "response": {
10361036 "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
1037− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
1037+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
10381038 "number": 14,
10391039 "issue": 12,
10401040 "title": "Greeting should name the caller",
10461046 "namespace": "pulls",
10471047 "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
10481048 },
1049− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
1049+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
10501050 "branch": null,
1051− "headCommit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
1052− "mergeBase": null,
1053− "mergedBy": null,
1054− "mergedAt": null,
1055− "supersededBy": null,
1056− "checkStatus": null,
1051+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
1052+ "merge_base": null,
1053+ "merged_by": null,
1054+ "merged_at": null,
1055+ "superseded_by": null,
1056+ "check_status": null,
10571057 "files": [
10581058 {
10591059 "path": "src/main.rs",
10701070 "verified": false,
10711071 "workspaces": []
10721072 },
1073− "createdAt": "2026-10-01T18:20:02.117Z",
1074− "updatedAt": "2026-10-01T18:33:10.398Z"
1073+ "created_at": "2026-10-01T18:20:02.117Z",
1074+ "updated_at": "2026-10-01T18:33:10.398Z"
10751075 }
10761076 },
10771077 "review_pull_request": {
10981098 "path": null,
10991099 "line": null,
11001100 "verdict": "request_changes",
1101− "createdAt": "2026-10-01T18:40:05.019Z"
1101+ "created_at": "2026-10-01T18:40:05.019Z"
11021102 }
11031103 },
11041104 "merge_pull_request": {
11071107 },
11081108 "response": {
11091109 "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
1110− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
1110+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
11111111 "number": 14,
11121112 "issue": 12,
11131113 "title": "Greeting should name the caller",
11191119 "namespace": "pulls",
11201120 "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
11211121 },
1122− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
1122+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
11231123 "branch": null,
1124− "headCommit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
1125− "mergeBase": "3a7e9c1b5d2f4a6c8e0b2d4f6a8c0e2b4d6f8a1c",
1126− "mergedBy": "syntaqx",
1127− "mergedAt": "2026-10-01T18:52:17.093Z",
1128− "supersededBy": null,
1129− "checkStatus": "passed",
1124+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
1125+ "merge_base": "3a7e9c1b5d2f4a6c8e0b2d4f6a8c0e2b4d6f8a1c",
1126+ "merged_by": "syntaqx",
1127+ "merged_at": "2026-10-01T18:52:17.093Z",
1128+ "superseded_by": null,
1129+ "check_status": "passed",
11301130 "files": [
11311131 {
11321132 "path": "src/main.rs",
11451145 "verified": false,
11461146 "workspaces": []
11471147 },
1148− "createdAt": "2026-10-01T18:20:02.117Z",
1149− "updatedAt": "2026-10-01T18:52:17.093Z"
1148+ "created_at": "2026-10-01T18:20:02.117Z",
1149+ "updated_at": "2026-10-01T18:52:17.093Z"
11501150 },
1151− "notes": "A draft, or one whose checks have not passed, answers `409`. A pull request still `open` in the response has not landed yet: `landing` is true on it while it is brought up to date, and [get the merge queue](/reference/api/pull-requests/get-merge-queue/) shows it waiting in the [merge queue](/guides/merge-queue/). Merging records the pull request in the issue's `resolvedBy` and sets `supersededBy` on the pull requests it closes."
1151+ "notes": "A draft, or one whose checks have not passed, answers `409`. A pull request still `open` in the response has not landed yet: `landing` is true on it while it is brought up to date, and [get the merge queue](/reference/api/pull-requests/get-merge-queue/) shows it waiting in the [merge queue](/guides/merge-queue/). Merging records the pull request in the issue's `resolved_by` and sets `superseded_by` on the pull requests it closes."
11521152 },
11531153 "close_pull_request": {
11541154 "response": {
11551155 "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
1156− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
1156+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
11571157 "number": 14,
11581158 "issue": 12,
11591159 "title": "Greeting should name the caller",
11651165 "namespace": "pulls",
11661166 "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
11671167 },
1168− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
1168+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
11691169 "branch": null,
1170− "headCommit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
1171− "mergeBase": null,
1172− "mergedBy": null,
1173− "mergedAt": null,
1174− "supersededBy": null,
1175− "checkStatus": null,
1170+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
1171+ "merge_base": null,
1172+ "merged_by": null,
1173+ "merged_at": null,
1174+ "superseded_by": null,
1175+ "check_status": null,
11761176 "files": [
11771177 {
11781178 "path": "src/main.rs",
11891189 "verified": false,
11901190 "workspaces": []
11911191 },
1192− "createdAt": "2026-10-01T18:20:02.117Z",
1193− "updatedAt": "2026-10-01T19:02:48.760Z"
1192+ "created_at": "2026-10-01T18:20:02.117Z",
1193+ "updated_at": "2026-10-01T19:02:48.760Z"
11941194 }
11951195 },
11961196 "get_merge_queue": {
12041204 "agent": "claude-code",
12051205 "state": "testing",
12061206 "ahead": [],
1207− "baseCommit": "3a7e9c1b5d2f4a6c8e0b2d4f6a8c0e2b4d6f8a1c",
1208− "combinedCommit": null,
1207+ "base_commit": "3a7e9c1b5d2f4a6c8e0b2d4f6a8c0e2b4d6f8a1c",
1208+ "combined_commit": null,
12091209 "error": null,
12101210 "results": [],
1211− "enqueuedBy": "syntaqx",
1212− "createdAt": "2026-10-04T15:31:20.441Z",
1213− "finishedAt": null
1211+ "enqueued_by": "syntaqx",
1212+ "created_at": "2026-10-04T15:31:20.441Z",
1213+ "finished_at": null
12141214 }
12151215 ],
12161216 "recent": [
12211221 "agent": "g1t-agent",
12221222 "state": "landed",
12231223 "ahead": [],
1224− "baseCommit": "4b1d7e9c2a5f8e3d6c0b9a7e5d3c1b8a6f4e2d0c",
1225− "combinedCommit": "c3e8a1f6d4b2097e5c3a1f8d6b4e2c0a9f7d5b3e",
1224+ "base_commit": "4b1d7e9c2a5f8e3d6c0b9a7e5d3c1b8a6f4e2d0c",
1225+ "combined_commit": "c3e8a1f6d4b2097e5c3a1f8d6b4e2c0a9f7d5b3e",
12261226 "error": null,
12271227 "results": [
12281228 {
12291229 "command": "cargo test",
12301230 "passed": true,
1231− "exitCode": 0,
1231+ "exit_code": 0,
12321232 "output": "test result: ok. 12 passed; 0 failed\n",
1233− "durationMs": 48211
1233+ "duration_ms": 48211
12341234 }
12351235 ],
1236− "enqueuedBy": "g1t",
1237− "createdAt": "2026-10-04T14:02:09.876Z",
1238− "finishedAt": "2026-10-04T14:09:55.102Z"
1236+ "enqueued_by": "g1t",
1237+ "created_at": "2026-10-04T14:02:09.876Z",
1238+ "finished_at": "2026-10-04T14:09:55.102Z"
12391239 }
12401240 ]
12411241 },
12491249 "id": "msg_01m43tx5egeh4t3f9zcnjenqvz",
12501250 "author": "syntaqx",
12511251 "body": "Keep \"world\" as the default when no name is given.",
1252− "createdAt": "2026-10-01T18:25:00.310Z",
1253− "deliveredAt": null,
1252+ "created_at": "2026-10-01T18:25:00.310Z",
1253+ "delivered_at": null,
12541254 "kind": "message",
1255− "fromNumber": null,
1256− "toNumber": 14,
1255+ "from_number": null,
1256+ "to_number": 14,
12571257 "answer": null,
12581258 "declined": false
12591259 },
1260− "notes": "The response is the message. `deliveredAt` is set once the agent has read it."
1260+ "notes": "The response is the message. `delivered_at` is set once the agent has read it."
12611261 },
12621262 "answer_message": {
12631263 "params": {
12701270 "id": "msg_01m43t66tde8hs2vpxhh3v8tqw",
12711271 "author": "g1t-agent",
12721272 "body": "Are you renaming greet() in src/lib.rs? I need to call it from #16.",
1273− "createdAt": "2026-10-01T18:58:12.301Z",
1274− "deliveredAt": "2026-10-01T18:58:40.117Z",
1273+ "created_at": "2026-10-01T18:58:12.301Z",
1274+ "delivered_at": "2026-10-01T18:58:40.117Z",
12751275 "kind": "question",
1276− "fromNumber": 16,
1277− "toNumber": 14,
1276+ "from_number": 16,
1277+ "to_number": 14,
12781278 "answer": "No. greet() keeps its name; only greet_named() is added.",
12791279 "declined": false
12801280 },
12861286 "id": "msg_01m43tx5egeh4t3f9zcnjenqvz",
12871287 "author": "syntaqx",
12881288 "body": "Keep \"world\" as the default when no name is given.",
1289− "createdAt": "2026-10-01T18:25:00.310Z",
1290− "deliveredAt": "2026-10-01T18:25:31.007Z",
1289+ "created_at": "2026-10-01T18:25:00.310Z",
1290+ "delivered_at": "2026-10-01T18:25:31.007Z",
12911291 "kind": "message",
1292− "fromNumber": null,
1293− "toNumber": 14,
1292+ "from_number": null,
1293+ "to_number": 14,
12941294 "answer": null,
12951295 "declined": false
12961296 }
13661366 "default": false
13671367 }
13681368 },
1369− "lastRun": {
1369+ "last_run": {
13701370 "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
1371− "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
1371+ "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
13721372 "path": ".g1t/workflows/ci.yml",
13731373 "name": "CI",
13741374 "title": "Greeting should name the caller",
13821382 "conclusion": "success",
13831383 "error": null,
13841384 "actor": "syntaqx",
1385− "createdAt": "2026-10-04T15:42:07.318Z",
1386− "startedAt": "2026-10-04T15:42:09.020Z",
1387− "finishedAt": "2026-10-04T15:44:31.877Z"
1385+ "created_at": "2026-10-04T15:42:07.318Z",
1386+ "started_at": "2026-10-04T15:42:09.020Z",
1387+ "finished_at": "2026-10-04T15:44:31.877Z"
13881388 }
13891389 },
13901390 {
13981398 "error": null,
13991399 "notes": [],
14001400 "dispatch": null,
1401− "lastRun": null
1401+ "last_run": null
14021402 }
14031403 ],
14041404 "notes": "`state` is `active` or `disabled`. `dispatch` holds the `workflow_dispatch` inputs, or is `null` when the workflow cannot be run by hand. See [GitHub Actions](/guides/actions/)."
14121412 "response": [
14131413 {
14141414 "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
1415− "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
1415+ "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
14161416 "path": ".g1t/workflows/ci.yml",
14171417 "name": "CI",
14181418 "title": "Greeting should name the caller",
14261426 "conclusion": "success",
14271427 "error": null,
14281428 "actor": "syntaqx",
1429− "createdAt": "2026-10-04T15:42:07.318Z",
1430− "startedAt": "2026-10-04T15:42:09.020Z",
1431− "finishedAt": "2026-10-04T15:44:31.877Z"
1429+ "created_at": "2026-10-04T15:42:07.318Z",
1430+ "started_at": "2026-10-04T15:42:09.020Z",
1431+ "finished_at": "2026-10-04T15:44:31.877Z"
14321432 }
14331433 ],
14341434 "notes": "A run's `status` is `queued`, `pending` (waiting for its concurrency group), `in_progress` or `completed`; `conclusion` is set once it completes."
14441444 "response": [
14451445 {
14461446 "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
1447− "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
1447+ "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
14481448 "path": ".g1t/workflows/ci.yml",
14491449 "name": "CI",
14501450 "title": "Greeting should name the caller",
14581458 "conclusion": "success",
14591459 "error": null,
14601460 "actor": "syntaqx",
1461− "createdAt": "2026-10-04T15:42:07.318Z",
1462− "startedAt": "2026-10-04T15:42:09.020Z",
1463− "finishedAt": "2026-10-04T15:44:31.877Z"
1461+ "created_at": "2026-10-04T15:42:07.318Z",
1462+ "started_at": "2026-10-04T15:42:09.020Z",
1463+ "finished_at": "2026-10-04T15:44:31.877Z"
14641464 }
14651465 ]
14661466 },
14711471 "response": {
14721472 "run": {
14731473 "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
1474− "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
1474+ "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
14751475 "path": ".g1t/workflows/ci.yml",
14761476 "name": "CI",
14771477 "title": "Greeting should name the caller",
14851485 "conclusion": "failure",
14861486 "error": null,
14871487 "actor": "syntaqx",
1488− "createdAt": "2026-10-04T15:42:07.318Z",
1489− "startedAt": "2026-10-04T15:42:09.020Z",
1490− "finishedAt": "2026-10-04T15:44:31.877Z"
1488+ "created_at": "2026-10-04T15:42:07.318Z",
1489+ "started_at": "2026-10-04T15:42:09.020Z",
1490+ "finished_at": "2026-10-04T15:44:31.877Z"
14911491 },
14921492 "jobs": [
14931493 {
14941494 "id": "job_01kpx7b3d0e4f8g2h6j0k4m8ns",
1495− "runId": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
1495+ "run_id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
14961496 "key": "test",
14971497 "name": "test",
14981498 "needs": [],
15041504 "name": "Run actions/checkout@v4",
15051505 "status": "completed",
15061506 "conclusion": "success",
1507− "startedAt": "2026-10-04T15:42:10.101Z",
1508− "finishedAt": "2026-10-04T15:42:12.433Z"
1507+ "started_at": "2026-10-04T15:42:10.101Z",
1508+ "finished_at": "2026-10-04T15:42:12.433Z"
15091509 },
15101510 {
15111511 "number": 2,
15121512 "name": "Run cargo test",
15131513 "status": "completed",
15141514 "conclusion": "failure",
1515− "startedAt": "2026-10-04T15:42:12.440Z",
1516− "finishedAt": "2026-10-04T15:44:30.902Z"
1515+ "started_at": "2026-10-04T15:42:12.440Z",
1516+ "finished_at": "2026-10-04T15:44:30.902Z"
15171517 }
15181518 ],
15191519 "annotations": [
15261526 }
15271527 ],
15281528 "reason": null,
1529− "startedAt": "2026-10-04T15:42:09.020Z",
1530− "finishedAt": "2026-10-04T15:44:31.002Z"
1529+ "started_at": "2026-10-04T15:42:09.020Z",
1530+ "finished_at": "2026-10-04T15:44:31.002Z"
15311531 },
15321532 {
15331533 "id": "job_01kpx7b3d0e4f8g2h6j0k4m8nt",
1534− "runId": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
1534+ "run_id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
15351535 "key": "deploy",
15361536 "name": "deploy",
15371537 "needs": [
15421542 "steps": [],
15431543 "annotations": [],
15441544 "reason": "A job it needs did not succeed.",
1545− "startedAt": null,
1546− "finishedAt": "2026-10-04T15:44:31.877Z"
1545+ "started_at": null,
1546+ "finished_at": "2026-10-04T15:44:31.877Z"
15471547 }
15481548 ],
15491549 "notes": [
15911591 },
15921592 "response": {
15931593 "id": "run_01kpx8d4e7f0g3h6j9k2m5n8pq",
1594− "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
1594+ "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
15951595 "path": ".g1t/workflows/ci.yml",
15961596 "name": "CI",
15971597 "title": "CI run by syntaqx",
16051605 "conclusion": null,
16061606 "error": null,
16071607 "actor": "syntaqx",
1608− "createdAt": "2026-10-04T16:30:00.512Z",
1609− "startedAt": null,
1610− "finishedAt": null
1608+ "created_at": "2026-10-04T16:30:00.512Z",
1609+ "started_at": null,
1610+ "finished_at": null
16111611 }
16121612 },
16131613 "cancel_workflow_run": {
16161616 },
16171617 "response": {
16181618 "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
1619− "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
1619+ "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
16201620 "path": ".g1t/workflows/ci.yml",
16211621 "name": "CI",
16221622 "title": "Greeting should name the caller",
16301630 "conclusion": "cancelled",
16311631 "error": null,
16321632 "actor": "syntaqx",
1633− "createdAt": "2026-10-04T15:42:07.318Z",
1634− "startedAt": "2026-10-04T15:42:09.020Z",
1635− "finishedAt": "2026-10-04T15:44:31.877Z"
1633+ "created_at": "2026-10-04T15:42:07.318Z",
1634+ "started_at": "2026-10-04T15:42:09.020Z",
1635+ "finished_at": "2026-10-04T15:44:31.877Z"
16361636 }
16371637 },
16381638 "rerun_workflow_run": {
16441644 },
16451645 "response": {
16461646 "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
1647− "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
1647+ "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
16481648 "path": ".g1t/workflows/ci.yml",
16491649 "name": "CI",
16501650 "title": "Greeting should name the caller",
16581658 "conclusion": null,
16591659 "error": null,
16601660 "actor": "syntaqx",
1661− "createdAt": "2026-10-04T15:42:07.318Z",
1662− "startedAt": null,
1663− "finishedAt": null
1661+ "created_at": "2026-10-04T15:42:07.318Z",
1662+ "started_at": null,
1663+ "finished_at": null
16641664 },
16651665 "notes": "The run keeps its id; `attempt` goes up by one."
16661666 },
16701670 },
16711671 "response": {
16721672 "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
1673− "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
1673+ "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
16741674 "path": ".g1t/workflows/ci.yml",
16751675 "name": "CI",
16761676 "title": "Greeting should name the caller",
16841684 "conclusion": null,
16851685 "error": null,
16861686 "actor": "syntaqx",
1687− "createdAt": "2026-10-04T15:42:07.318Z",
1688− "startedAt": null,
1689− "finishedAt": null
1687+ "created_at": "2026-10-04T15:42:07.318Z",
1688+ "started_at": null,
1689+ "finished_at": null
16901690 }
16911691 },
16921692 "update_workflow": {
17071707 "error": null,
17081708 "notes": [],
17091709 "dispatch": null,
1710− "lastRun": null
1710+ "last_run": null
17111711 }
17121712 },
17131713 "enable_workflow": {
17251725 "error": null,
17261726 "notes": [],
17271727 "dispatch": null,
1728− "lastRun": null
1728+ "last_run": null
17291729 }
17301730 },
17311731 "disable_workflow": {
17431743 "error": null,
17441744 "notes": [],
17451745 "dispatch": null,
1746− "lastRun": null
1746+ "last_run": null
17471747 }
17481748 },
17491749 "list_actions_secrets": {
17541754 "kind": "secret",
17551755 "value": null,
17561756 "scope": "workspace",
1757− "updatedAt": "2026-10-01T09:12:44.020Z",
1758− "availableTo": [
1757+ "updated_at": "2026-10-01T09:12:44.020Z",
1758+ "available_to": [
17591759 "workflows"
17601760 ],
17611761 "environments": [],
17621762 "projects": [],
17631763 "note": null,
1764− "updatedBy": "syntaqx"
1764+ "updated_by": "syntaqx"
17651765 },
17661766 {
17671767 "id": "set_01kpv9q4r7s0t3v6w9x2y5z8ac",
17691769 "kind": "secret",
17701770 "value": null,
17711771 "scope": "project",
1772− "updatedAt": "2026-10-04T15:42:07.318Z",
1773− "availableTo": [
1772+ "updated_at": "2026-10-04T15:42:07.318Z",
1773+ "available_to": [
17741774 "workflows",
17751775 "deployments"
17761776 ],
17791779 ],
17801780 "projects": [],
17811781 "note": "Rotate in the Stripe dashboard.",
1782− "updatedBy": "syntaqx"
1782+ "updated_by": "syntaqx"
17831783 }
17841784 ],
17851785 "notes": "Values are never returned. A repository's list includes the workspace's rows that reach it, with `scope` set to `workspace`. Empty `environments` means every environment."
17951795 "kind": "secret",
17961796 "value": null,
17971797 "scope": "workspace",
1798− "updatedAt": "2026-10-04T15:42:07.318Z",
1799− "availableTo": [
1798+ "updated_at": "2026-10-04T15:42:07.318Z",
1799+ "available_to": [
18001800 "workflows"
18011801 ],
18021802 "environments": [],
18031803 "projects": [],
18041804 "note": null,
1805− "updatedBy": "syntaqx"
1805+ "updated_by": "syntaqx"
18061806 }
18071807 ],
18081808 "notes": "Values are never returned. A repository's list includes the workspace's rows that reach it, with `scope` set to `workspace`. Empty `environments` means every environment."
18271827 "kind": "secret",
18281828 "value": null,
18291829 "scope": "project",
1830− "updatedAt": "2026-10-04T15:42:07.318Z",
1831− "availableTo": [
1830+ "updated_at": "2026-10-04T15:42:07.318Z",
1831+ "available_to": [
18321832 "workflows",
18331833 "deployments"
18341834 ],
18371837 ],
18381838 "projects": [],
18391839 "note": null,
1840− "updatedBy": "syntaqx"
1840+ "updated_by": "syntaqx"
18411841 },
18421842 "notes": "Keys are uppercased. See [secrets and variables](/guides/secrets-and-variables/)."
18431843 },
18581858 "kind": "secret",
18591859 "value": null,
18601860 "scope": "workspace",
1861− "updatedAt": "2026-10-04T15:42:07.318Z",
1862− "availableTo": [
1861+ "updated_at": "2026-10-04T15:42:07.318Z",
1862+ "available_to": [
18631863 "workflows"
18641864 ],
18651865 "environments": [],
18671867 "hello"
18681868 ],
18691869 "note": null,
1870− "updatedBy": "syntaqx"
1870+ "updated_by": "syntaqx"
18711871 }
18721872 },
18731873 "delete_actions_secret": {
18911891 "kind": "variable",
18921892 "value": "20",
18931893 "scope": "project",
1894− "updatedAt": "2026-10-04T15:42:07.318Z",
1895− "availableTo": [
1894+ "updated_at": "2026-10-04T15:42:07.318Z",
1895+ "available_to": [
18961896 "workflows",
18971897 "deployments"
18981898 ],
18991899 "environments": [],
19001900 "projects": [],
19011901 "note": null,
1902− "updatedBy": "syntaqx"
1902+ "updated_by": "syntaqx"
19031903 }
19041904 ],
19051905 "notes": "A repository's list includes the workspace's rows that reach it, with `scope` set to `workspace`. Empty `environments` means every environment."
19151915 "kind": "variable",
19161916 "value": "20",
19171917 "scope": "workspace",
1918− "updatedAt": "2026-10-04T15:42:07.318Z",
1919− "availableTo": [
1918+ "updated_at": "2026-10-04T15:42:07.318Z",
1919+ "available_to": [
19201920 "workflows",
19211921 "deployments"
19221922 ],
19231923 "environments": [],
19241924 "projects": [],
19251925 "note": null,
1926− "updatedBy": "syntaqx"
1926+ "updated_by": "syntaqx"
19271927 }
19281928 ]
19291929 },
19381938 "kind": "variable",
19391939 "value": "20",
19401940 "scope": "project",
1941− "updatedAt": "2026-10-04T15:42:07.318Z",
1942− "availableTo": [
1941+ "updated_at": "2026-10-04T15:42:07.318Z",
1942+ "available_to": [
19431943 "workflows",
19441944 "deployments"
19451945 ],
19461946 "environments": [],
19471947 "projects": [],
19481948 "note": null,
1949− "updatedBy": "syntaqx"
1949+ "updated_by": "syntaqx"
19501950 },
19511951 "notes": "GitHub's clients send the key as `name`; that is accepted too."
19521952 },
19641964 "kind": "variable",
19651965 "value": "20",
19661966 "scope": "workspace",
1967− "updatedAt": "2026-10-04T15:42:07.318Z",
1968− "availableTo": [
1967+ "updated_at": "2026-10-04T15:42:07.318Z",
1968+ "available_to": [
19691969 "workflows",
19701970 "deployments"
19711971 ],
19721972 "environments": [],
19731973 "projects": [],
19741974 "note": null,
1975− "updatedBy": "syntaqx"
1975+ "updated_by": "syntaqx"
19761976 }
19771977 },
19781978 "update_actions_variable": {
19881988 "kind": "variable",
19891989 "value": "22",
19901990 "scope": "project",
1991− "updatedAt": "2026-10-04T15:42:07.318Z",
1992− "availableTo": [
1991+ "updated_at": "2026-10-04T15:42:07.318Z",
1992+ "available_to": [
19931993 "workflows",
19941994 "deployments"
19951995 ],
19961996 "environments": [],
19971997 "projects": [],
19981998 "note": null,
1999− "updatedBy": "syntaqx"
1999+ "updated_by": "syntaqx"
20002000 }
20012001 },
20022002 "update_actions_variable_for_workspace": {
20132013 "kind": "variable",
20142014 "value": "22",
20152015 "scope": "workspace",
2016− "updatedAt": "2026-10-04T15:42:07.318Z",
2017− "availableTo": [
2016+ "updated_at": "2026-10-04T15:42:07.318Z",
2017+ "available_to": [
20182018 "workflows",
20192019 "deployments"
20202020 ],
20212021 "environments": [],
20222022 "projects": [],
20232023 "note": null,
2024− "updatedBy": "syntaqx"
2024+ "updated_by": "syntaqx"
20252025 }
20262026 },
20272027 "delete_actions_variable": {
20502050 "pull.merged"
20512051 ],
20522052 "active": true,
2053− "secretHint": "…9c2e",
2054− "createdBy": "syntaqx",
2055− "createdAt": "2026-10-04T15:42:07.318Z",
2056− "lastStatus": "delivered",
2057− "lastDeliveredAt": "2026-10-04T15:42:07.611Z"
2053+ "secret_hint": "…9c2e",
2054+ "created_by": "syntaqx",
2055+ "created_at": "2026-10-04T15:42:07.318Z",
2056+ "last_status": "delivered",
2057+ "last_delivered_at": "2026-10-04T15:42:07.611Z"
20582058 }
20592059 ]
20602060 },
20742074 "pull.merged"
20752075 ],
20762076 "active": true,
2077− "secretHint": "…9c2e",
2078− "createdBy": "syntaqx",
2079− "createdAt": "2026-10-04T15:42:07.318Z",
2080− "lastStatus": "delivered",
2081− "lastDeliveredAt": "2026-10-04T15:42:07.611Z"
2077+ "secret_hint": "…9c2e",
2078+ "created_by": "syntaqx",
2079+ "created_at": "2026-10-04T15:42:07.318Z",
2080+ "last_status": "delivered",
2081+ "last_delivered_at": "2026-10-04T15:42:07.611Z"
20822082 }
20832083 ]
20842084 },
21022102 "pull.merged"
21032103 ],
21042104 "active": true,
2105− "secretHint": "…9c2e",
2106− "createdBy": "syntaqx",
2107− "createdAt": "2026-10-04T15:42:07.318Z",
2108− "lastStatus": "delivered",
2109− "lastDeliveredAt": "2026-10-04T15:42:07.611Z"
2105+ "secret_hint": "…9c2e",
2106+ "created_by": "syntaqx",
2107+ "created_at": "2026-10-04T15:42:07.318Z",
2108+ "last_status": "delivered",
2109+ "last_delivered_at": "2026-10-04T15:42:07.611Z"
21102110 },
21112111 "secret": "whsec_…"
21122112 },
2113− "notes": "A ping is sent before the response, so `lastStatus` is usually set already. When g1t made the signing secret, `secret` holds it: it is shown only this once. See [webhooks](/guides/webhooks/)."
2113+ "notes": "A ping is sent before the response, so `last_status` is usually set already. When g1t made the signing secret, `secret` holds it: it is shown only this once. See [webhooks](/guides/webhooks/)."
21142114 },
21152115 "create_webhook_for_workspace": {
21162116 "params": {
21302130 "*"
21312131 ],
21322132 "active": true,
2133− "secretHint": "…9c2e",
2134− "createdBy": "syntaqx",
2135− "createdAt": "2026-10-04T15:42:07.318Z",
2136− "lastStatus": "delivered",
2137− "lastDeliveredAt": "2026-10-04T15:42:07.611Z"
2133+ "secret_hint": "…9c2e",
2134+ "created_by": "syntaqx",
2135+ "created_at": "2026-10-04T15:42:07.318Z",
2136+ "last_status": "delivered",
2137+ "last_delivered_at": "2026-10-04T15:42:07.611Z"
21382138 },
21392139 "secret": "whsec_…"
21402140 },
2141− "notes": "A ping is sent before the response, so `lastStatus` is usually set already. When g1t made the signing secret, `secret` holds it: it is shown only this once. See [webhooks](/guides/webhooks/)."
2141+ "notes": "A ping is sent before the response, so `last_status` is usually set already. When g1t made the signing secret, `secret` holds it: it is shown only this once. See [webhooks](/guides/webhooks/)."
21422142 },
21432143 "update_webhook": {
21442144 "params": {
21582158 "pull.merged"
21592159 ],
21602160 "active": false,
2161− "secretHint": "…9c2e",
2162− "createdBy": "syntaqx",
2163− "createdAt": "2026-10-04T15:42:07.318Z",
2164− "lastStatus": "delivered",
2165− "lastDeliveredAt": "2026-10-04T15:42:07.611Z"
2161+ "secret_hint": "…9c2e",
2162+ "created_by": "syntaqx",
2163+ "created_at": "2026-10-04T15:42:07.318Z",
2164+ "last_status": "delivered",
2165+ "last_delivered_at": "2026-10-04T15:42:07.611Z"
21662166 }
21672167 },
21682168 "update_webhook_for_workspace": {
21842184 "pull.merged"
21852185 ],
21862186 "active": false,
2187− "secretHint": "…9c2e",
2188− "createdBy": "syntaqx",
2189− "createdAt": "2026-10-04T15:42:07.318Z",
2190− "lastStatus": "delivered",
2191− "lastDeliveredAt": "2026-10-04T15:42:07.611Z"
2187+ "secret_hint": "…9c2e",
2188+ "created_by": "syntaqx",
2189+ "created_at": "2026-10-04T15:42:07.318Z",
2190+ "last_status": "delivered",
2191+ "last_delivered_at": "2026-10-04T15:42:07.611Z"
21922192 }
21932193 },
21942194 "delete_webhook": {
22102210 },
22112211 "response": {
22122212 "id": "dlv_01kpx5p9v2j7k3m8n4p5q6r7st",
2213− "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
2214− "eventId": "",
2213+ "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
2214+ "event_id": "",
22152215 "event": "ping",
22162216 "status": "delivered",
22172217 "attempts": 1,
2218− "responseStatus": 200,
2219− "responseBody": "ok",
2218+ "response_status": 200,
2219+ "response_body": "ok",
22202220 "error": null,
2221− "durationMs": 184,
2221+ "duration_ms": 184,
22222222 "payload": "{\"hook\":{\"events\":[\"git.push\",\"pull.merged\"],\"id\":\"hk_01kpx5p9s4h0t1w6x8y2a3b4cd\",\"url\":\"https://hooks.example.com/g1t\"},\"message\":\"g1t will send this webhook's events here.\",\"time\":\"2026-10-04T16:01:12.440Z\",\"type\":\"ping\"}",
2223− "createdAt": "2026-10-04T16:01:12.440Z",
2224− "deliveredAt": "2026-10-04T16:01:12.631Z",
2225− "nextAttemptAt": null
2223+ "created_at": "2026-10-04T16:01:12.440Z",
2224+ "delivered_at": "2026-10-04T16:01:12.631Z",
2225+ "next_attempt_at": null
22262226 },
22272227 "notes": "`payload` is the JSON that was sent, as a string."
22282228 },
22332233 },
22342234 "response": {
22352235 "id": "dlv_01kpx5p9v2j7k3m8n4p5q6r7st",
2236− "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
2237− "eventId": "",
2236+ "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
2237+ "event_id": "",
22382238 "event": "ping",
22392239 "status": "delivered",
22402240 "attempts": 1,
2241− "responseStatus": 200,
2242− "responseBody": "ok",
2241+ "response_status": 200,
2242+ "response_body": "ok",
22432243 "error": null,
2244− "durationMs": 184,
2244+ "duration_ms": 184,
22452245 "payload": "{\"hook\":{\"events\":[\"git.push\",\"pull.merged\"],\"id\":\"hk_01kpx5p9s4h0t1w6x8y2a3b4cd\",\"url\":\"https://hooks.example.com/g1t\"},\"message\":\"g1t will send this webhook's events here.\",\"time\":\"2026-10-04T16:01:12.440Z\",\"type\":\"ping\"}",
2246− "createdAt": "2026-10-04T16:01:12.440Z",
2247− "deliveredAt": "2026-10-04T16:01:12.631Z",
2248− "nextAttemptAt": null
2246+ "created_at": "2026-10-04T16:01:12.440Z",
2247+ "delivered_at": "2026-10-04T16:01:12.631Z",
2248+ "next_attempt_at": null
22492249 }
22502250 },
22512251 "list_webhook_deliveries": {
22552255 "response": [
22562256 {
22572257 "id": "dlv_01kpx6a2b3c4d5e6f7g8h9j0kz",
2258− "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
2259− "eventId": "",
2258+ "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
2259+ "event_id": "",
22602260 "event": "ping",
22612261 "status": "pending",
22622262 "attempts": 1,
2263− "responseStatus": 503,
2264− "responseBody": "Service Unavailable",
2263+ "response_status": 503,
2264+ "response_body": "Service Unavailable",
22652265 "error": null,
2266− "durationMs": 212,
2266+ "duration_ms": 212,
22672267 "payload": "{\"hook\":{\"events\":[\"git.push\",\"pull.merged\"],\"id\":\"hk_01kpx5p9s4h0t1w6x8y2a3b4cd\",\"url\":\"https://hooks.example.com/g1t\"},\"message\":\"g1t will send this webhook's events here.\",\"time\":\"2026-10-04T16:01:12.440Z\",\"type\":\"ping\"}",
2268− "createdAt": "2026-10-04T16:20:03.100Z",
2269− "deliveredAt": null,
2270− "nextAttemptAt": "2026-10-04T16:21:03.312Z"
2268+ "created_at": "2026-10-04T16:20:03.100Z",
2269+ "delivered_at": null,
2270+ "next_attempt_at": "2026-10-04T16:21:03.312Z"
22712271 },
22722272 {
22732273 "id": "dlv_01kpx5p9v2j7k3m8n4p5q6r7st",
2274− "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
2275− "eventId": "",
2274+ "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
2275+ "event_id": "",
22762276 "event": "ping",
22772277 "status": "delivered",
22782278 "attempts": 1,
2279− "responseStatus": 200,
2280− "responseBody": "ok",
2279+ "response_status": 200,
2280+ "response_body": "ok",
22812281 "error": null,
2282− "durationMs": 184,
2282+ "duration_ms": 184,
22832283 "payload": "{\"hook\":{\"events\":[\"git.push\",\"pull.merged\"],\"id\":\"hk_01kpx5p9s4h0t1w6x8y2a3b4cd\",\"url\":\"https://hooks.example.com/g1t\"},\"message\":\"g1t will send this webhook's events here.\",\"time\":\"2026-10-04T16:01:12.440Z\",\"type\":\"ping\"}",
2284− "createdAt": "2026-10-04T16:01:12.440Z",
2285− "deliveredAt": "2026-10-04T16:01:12.631Z",
2286− "nextAttemptAt": null
2284+ "created_at": "2026-10-04T16:01:12.440Z",
2285+ "delivered_at": "2026-10-04T16:01:12.631Z",
2286+ "next_attempt_at": null
22872287 }
22882288 ],
2289− "notes": "Returns at most 50, newest first. `status` is `pending`, `delivered` or `failed`; `nextAttemptAt` is set while it is pending."
2289+ "notes": "Returns at most 50, newest first. `status` is `pending`, `delivered` or `failed`; `next_attempt_at` is set while it is pending."
22902290 },
22912291 "list_webhook_deliveries_for_workspace": {
22922292 "params": {
22962296 "response": [
22972297 {
22982298 "id": "dlv_01kpx6a2b3c4d5e6f7g8h9j0kz",
2299− "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
2300− "eventId": "",
2299+ "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
2300+ "event_id": "",
23012301 "event": "ping",
23022302 "status": "pending",
23032303 "attempts": 1,
2304− "responseStatus": 503,
2305− "responseBody": "Service Unavailable",
2304+ "response_status": 503,
2305+ "response_body": "Service Unavailable",
23062306 "error": null,
2307− "durationMs": 212,
2307+ "duration_ms": 212,
23082308 "payload": "{\"hook\":{\"events\":[\"git.push\",\"pull.merged\"],\"id\":\"hk_01kpx5p9s4h0t1w6x8y2a3b4cd\",\"url\":\"https://hooks.example.com/g1t\"},\"message\":\"g1t will send this webhook's events here.\",\"time\":\"2026-10-04T16:01:12.440Z\",\"type\":\"ping\"}",
2309− "createdAt": "2026-10-04T16:20:03.100Z",
2310− "deliveredAt": null,
2311− "nextAttemptAt": "2026-10-04T16:21:03.312Z"
2309+ "created_at": "2026-10-04T16:20:03.100Z",
2310+ "delivered_at": null,
2311+ "next_attempt_at": "2026-10-04T16:21:03.312Z"
23122312 },
23132313 {
23142314 "id": "dlv_01kpx5p9v2j7k3m8n4p5q6r7st",
2315− "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
2316− "eventId": "",
2315+ "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
2316+ "event_id": "",
23172317 "event": "ping",
23182318 "status": "delivered",
23192319 "attempts": 1,
2320− "responseStatus": 200,
2321− "responseBody": "ok",
2320+ "response_status": 200,
2321+ "response_body": "ok",
23222322 "error": null,
2323− "durationMs": 184,
2323+ "duration_ms": 184,
23242324 "payload": "{\"hook\":{\"events\":[\"git.push\",\"pull.merged\"],\"id\":\"hk_01kpx5p9s4h0t1w6x8y2a3b4cd\",\"url\":\"https://hooks.example.com/g1t\"},\"message\":\"g1t will send this webhook's events here.\",\"time\":\"2026-10-04T16:01:12.440Z\",\"type\":\"ping\"}",
2325− "createdAt": "2026-10-04T16:01:12.440Z",
2326− "deliveredAt": "2026-10-04T16:01:12.631Z",
2327− "nextAttemptAt": null
2325+ "created_at": "2026-10-04T16:01:12.440Z",
2326+ "delivered_at": "2026-10-04T16:01:12.631Z",
2327+ "next_attempt_at": null
23282328 }
23292329 ]
23302330 },
23352335 },
23362336 "response": {
23372337 "id": "dlv_01kpx6c8d1e2f3g4h5j6k7m8np",
2338− "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
2339− "eventId": "",
2338+ "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
2339+ "event_id": "",
23402340 "event": "ping",
23412341 "status": "delivered",
23422342 "attempts": 1,
2343− "responseStatus": 200,
2344− "responseBody": "ok",
2343+ "response_status": 200,
2344+ "response_body": "ok",
23452345 "error": null,
2346− "durationMs": 171,
2346+ "duration_ms": 171,
23472347 "payload": "{\"hook\":{\"events\":[\"git.push\",\"pull.merged\"],\"id\":\"hk_01kpx5p9s4h0t1w6x8y2a3b4cd\",\"url\":\"https://hooks.example.com/g1t\"},\"message\":\"g1t will send this webhook's events here.\",\"time\":\"2026-10-04T16:01:12.440Z\",\"type\":\"ping\"}",
2348− "createdAt": "2026-10-04T16:25:40.007Z",
2349− "deliveredAt": "2026-10-04T16:25:40.178Z",
2350− "nextAttemptAt": null
2348+ "created_at": "2026-10-04T16:25:40.007Z",
2349+ "delivered_at": "2026-10-04T16:25:40.178Z",
2350+ "next_attempt_at": null
23512351 },
23522352 "notes": "The response is the new delivery."
23532353 },
23592359 },
23602360 "response": {
23612361 "id": "dlv_01kpx6c8d1e2f3g4h5j6k7m8np",
2362− "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
2363− "eventId": "",
2362+ "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
2363+ "event_id": "",
23642364 "event": "ping",
23652365 "status": "delivered",
23662366 "attempts": 1,
2367− "responseStatus": 200,
2368− "responseBody": "ok",
2367+ "response_status": 200,
2368+ "response_body": "ok",
23692369 "error": null,
2370− "durationMs": 171,
2370+ "duration_ms": 171,
23712371 "payload": "{\"hook\":{\"events\":[\"git.push\",\"pull.merged\"],\"id\":\"hk_01kpx5p9s4h0t1w6x8y2a3b4cd\",\"url\":\"https://hooks.example.com/g1t\"},\"message\":\"g1t will send this webhook's events here.\",\"time\":\"2026-10-04T16:01:12.440Z\",\"type\":\"ping\"}",
2372− "createdAt": "2026-10-04T16:25:40.007Z",
2373− "deliveredAt": "2026-10-04T16:25:40.178Z",
2374− "nextAttemptAt": null
2372+ "created_at": "2026-10-04T16:25:40.007Z",
2373+ "delivered_at": "2026-10-04T16:25:40.178Z",
2374+ "next_attempt_at": null
23752375 }
23762376 },
23772377 "list_integrations": {
23872387 "name": "Anthropic",
23882388 "config": {
23892389 "assign": false,
2390− "writeBack": true
2390+ "write_back": true
23912391 },
2392− "secretHint": "…3f9a",
2393− "webhookUrl": null,
2394− "createdBy": "syntaqx",
2395− "createdAt": "2026-10-04T15:42:07.318Z",
2396− "lastUsedAt": "2026-10-04T15:42:08.102Z",
2397− "lastError": null,
2392+ "secret_hint": "…3f9a",
2393+ "webhook_url": null,
2394+ "created_by": "syntaqx",
2395+ "created_at": "2026-10-04T15:42:07.318Z",
2396+ "last_used_at": "2026-10-04T15:42:08.102Z",
2397+ "last_error": null,
23982398 "models": [
23992399 "claude-haiku-4-5",
24002400 "claude-sonnet-4-5"
24082408 "name": "Jira",
24092409 "config": {
24102410 "assign": false,
2411− "writeBack": true,
2411+ "write_back": true,
24122412 "site": "https://acme.atlassian.net",
24132413 "email": "syntaqx@example.com",
24142414 "keys": [
24152415 "TECH"
24162416 ]
24172417 },
2418− "secretHint": "…x7Qe",
2419− "webhookUrl": null,
2420− "createdBy": "syntaqx",
2421− "createdAt": "2026-10-04T15:42:07.318Z",
2422− "lastUsedAt": null,
2423− "lastError": null,
2418+ "secret_hint": "…x7Qe",
2419+ "webhook_url": null,
2420+ "created_by": "syntaqx",
2421+ "created_at": "2026-10-04T15:42:07.318Z",
2422+ "last_used_at": null,
2423+ "last_error": null,
24242424 "models": []
24252425 }
24262426 ],
2427− "notes": "`kind` is `models`, `alerts` or `tracker`. `secretHint` shows the end of the secret; the secret itself is never returned. `webhookUrl` is where an alert source sends its alerts."
2427+ "notes": "`kind` is `models`, `alerts` or `tracker`. `secret_hint` shows the end of the secret; the secret itself is never returned. `webhook_url` is where an alert source sends its alerts."
24282428 },
24292429 "connect_integration": {
24302430 "params": {
24482448 "repo": "syntaqx/hello",
24492449 "label": "bug",
24502450 "assign": false,
2451− "writeBack": true
2451+ "write_back": true
24522452 },
2453− "secretHint": null,
2454− "webhookUrl": "https://api.g1t.sh/hooks/con_01kpx4n8r3g9s0v5w7x1z2a3bd",
2455− "createdBy": "syntaqx",
2456− "createdAt": "2026-10-04T15:42:07.318Z",
2457− "lastUsedAt": null,
2458− "lastError": null,
2453+ "secret_hint": null,
2454+ "webhook_url": "https://api.g1t.sh/hooks/con_01kpx4n8r3g9s0v5w7x1z2a3bd",
2455+ "created_by": "syntaqx",
2456+ "created_at": "2026-10-04T15:42:07.318Z",
2457+ "last_used_at": null,
2458+ "last_error": null,
24592459 "models": []
24602460 },
2461− "signingSecret": "g1ts_9f2c4a7e1b0d3c6f8a2e5b7d9c1f4a6e8b0d2c4f6a8e1b3d"
2461+ "signing_secret": "g1ts_9f2c4a7e1b0d3c6f8a2e5b7d9c1f4a6e8b0d2c4f6a8e1b3d"
24622462 },
2463− "notes": "`signingSecret` is set only when g1t made it, for `datadog` and `webhook`, and is shown only this once. A model provider is tested as it is connected. See [integrations](/guides/integrations/) and [model providers](/guides/models/)."
2463+ "notes": "`signing_secret` is set only when g1t made it, for `datadog` and `webhook`, and is shown only this once. A model provider is tested as it is connected. See [integrations](/guides/integrations/) and [model providers](/guides/models/)."
24642464 },
24652465 "disconnect_integration": {
24662466 "params": {
24872487 "response": [
24882488 {
24892489 "task": "default",
2490− "connectionId": "con_01kpx3m7q2f8r9t4v6w0y1z2ab",
2490+ "connection_id": "con_01kpx3m7q2f8r9t4v6w0y1z2ab",
24912491 "model": "claude-sonnet-4-5"
24922492 },
24932493 {
24942494 "task": "review",
2495− "connectionId": null,
2495+ "connection_id": null,
24962496 "model": null
24972497 }
24982498 ]
25172517 "response": [
25182518 {
25192519 "task": "default",
2520− "connectionId": "con_01kpx3m7q2f8r9t4v6w0y1z2ab",
2520+ "connection_id": "con_01kpx3m7q2f8r9t4v6w0y1z2ab",
25212521 "model": "claude-sonnet-4-5"
25222522 },
25232523 {
25242524 "task": "review",
2525− "connectionId": null,
2525+ "connection_id": null,
25262526 "model": null
25272527 }
25282528 ],
25392539 "url": "https://acme.atlassian.net/browse/TECH-1234",
25402540 "status": "In Progress",
25412541 "body": "Customers with 3DS cards see a 500 at /pay.",
2542− "fetchedAt": "2026-10-04T15:42:07.318Z"
2542+ "fetched_at": "2026-10-04T15:42:07.318Z"
25432543 }
25442544 },
25452545 "import_issue": {
25552555 "url": "https://acme.atlassian.net/browse/TECH-1234",
25562556 "status": "In Progress",
25572557 "body": "Customers with 3DS cards see a 500 at /pay.",
2558− "fetchedAt": "2026-10-04T15:42:07.318Z"
2558+ "fetched_at": "2026-10-04T15:42:07.318Z"
25592559 },
25602560 "created": true
25612561 },
25802580 "kind": "gotcha",
25812581 "source": {
25822582 "kind": "run",
2583− "runId": "arn_01m43v2c1p9k8d7e6f5a4b3c2d",
2583+ "run_id": "arn_01m43v2c1p9k8d7e6f5a4b3c2d",
25842584 "repo": {
25852585 "namespace": "syntaqx",
25862586 "name": "hello"
25872587 },
25882588 "number": 14
25892589 },
2590− "createdBy": "g1t-agent",
2590+ "created_by": "g1t-agent",
25912591 "pinned": false,
2592− "createdAt": "2026-10-01T18:40:12.000Z",
2593− "updatedAt": "2026-10-01T18:40:12.000Z",
2594− "lastUsedAt": null
2592+ "created_at": "2026-10-01T18:40:12.000Z",
2593+ "updated_at": "2026-10-01T18:40:12.000Z",
2594+ "last_used_at": null
25952595 },
25962596 "notes": "Text that looks like a key, a token or a password is refused with `invalid`: memory is read by every agent in the workspace. Saving the same text again in the same scope returns the memory already kept."
25972597 },
26112611 "kind": "convention",
26122612 "source": {
26132613 "kind": "person",
2614− "runId": null,
2614+ "run_id": null,
26152615 "repo": null,
26162616 "number": null
26172617 },
2618− "createdBy": "syntaqx",
2618+ "created_by": "syntaqx",
26192619 "pinned": true,
2620− "createdAt": "2026-10-01T18:40:12.000Z",
2621− "updatedAt": "2026-10-01T18:40:12.000Z",
2622− "lastUsedAt": null
2620+ "created_at": "2026-10-01T18:40:12.000Z",
2621+ "updated_at": "2026-10-01T18:40:12.000Z",
2622+ "last_used_at": null
26232623 }
26242624 ]
26252625 },
26262626 "notes": "Members of the workspace only. Each memory returned is marked used, which keeps it near the front of what agents are given."
2627+ },
2628+ "search_context": {
2629+ "params": {
2630+ "workspace": "acme"
2631+ },
2632+ "query": {
2633+ "q": "how do we run the web tests",
2634+ "project": "web"
2635+ },
2636+ "response": {
2637+ "query": "how do we run the web tests",
2638+ "mode": "semantic",
2639+ "hits": [
2640+ {
2641+ "kind": "memory",
2642+ "id": "mem_01m4a0c2b7k3f9d1e5g8h2j6k4",
2643+ "title": "Gotcha",
2644+ "snippet": "The date tests fail unless TZ=UTC.",
2645+ "project": "web",
2646+ "url": "/acme/web/memory",
2647+ "score": 0.82,
2648+ "source": "AGENTS.md",
2649+ "by": "g1t",
2650+ "updated_at": "2026-10-04T21:10:02.000Z"
2651+ },
2652+ {
2653+ "kind": "doc",
2654+ "id": "ent_5f0c2a9e41d7b3c86a1e2f40:2",
2655+ "title": "Web (README.md)",
2656+ "snippet": "## Testing Run npm test. The end-to-end tests need the api running locally…",
2657+ "project": "web",
2658+ "url": "/acme/web/blob/main/README.md",
2659+ "score": 0.77,
2660+ "source": "README.md",
2661+ "by": null,
2662+ "updated_at": "2026-10-04T20:58:41.000Z"
2663+ },
2664+ {
2665+ "kind": "project",
2666+ "id": "ent_9b1d6f0a2c3e4b5d6e7f8a9b",
2667+ "title": "web",
2668+ "snippet": "The storefront. Written in TypeScript. Packages: @acme/web. Uses api. Owned by ana.",
2669+ "project": "web",
2670+ "url": "/acme/web",
2671+ "score": 0.71,
2672+ "source": "catalog",
2673+ "by": null,
2674+ "updated_at": "2026-10-04T20:58:41.000Z"
2675+ }
2676+ ]
2677+ },
2678+ "notes": "Give `workspace`, or `repo` as `owner/name` for its workspace. `kinds` narrows to some of `project`, `app`, `api`, `package`, `language`, `owner`, `environment`, `integration`, `doc`, `memory`, `issue` and `pull`; in a URL, comma-separated. `mode` is `text` when the search index could not answer and words were matched instead. Memory, and anything from a private project, is returned only to members of the workspace and its agents. A g1t agent searches its own workspace only."
2679+ },
2680+ "get_entity": {
2681+ "params": {
2682+ "workspace": "acme",
2683+ "kind": "project",
2684+ "id": "web"
2685+ },
2686+ "response": {
2687+ "entity": {
2688+ "id": "ent_9b1d6f0a2c3e4b5d6e7f8a9b",
2689+ "workspace": "acme",
2690+ "kind": "project",
2691+ "key": "web",
2692+ "name": "web",
2693+ "summary": "The storefront. Written in TypeScript. Packages: @acme/web. Uses api. Owned by ana.",
2694+ "project": "web",
2695+ "private": true,
2696+ "data": {
2697+ "repo": "acme/web",
2698+ "root_dir": "",
2699+ "default_branch": "main",
2700+ "languages": [
2701+ "TypeScript"
2702+ ],
2703+ "owners": [
2704+ "ana"
2705+ ],
2706+ "tests": true,
2707+ "test_commands": [
2708+ "npm test"
2709+ ],
2710+ "production_url": "https://web--acme.g1t.page"
2711+ },
2712+ "source": "scan",
2713+ "ref": "/acme/web",
2714+ "updated_at": "2026-10-04T20:58:41.000Z"
2715+ },
2716+ "relations": [
2717+ {
2718+ "kind": "depends_on",
2719+ "direction": "out",
2720+ "entity": {
2721+ "id": "ent_0a7c3e5b9d1f2a4c6e8b0d2f",
2722+ "workspace": "acme",
2723+ "kind": "project",
2724+ "key": "api",
2725+ "name": "api",
2726+ "summary": "The public API. Written in Rust.",
2727+ "project": "api",
2728+ "private": true,
2729+ "data": {},
2730+ "source": "scan",
2731+ "ref": "/acme/api",
2732+ "updated_at": "2026-10-04T20:57:12.000Z"
2733+ }
2734+ },
2735+ {
2736+ "kind": "owned_by",
2737+ "direction": "out",
2738+ "entity": {
2739+ "id": "ent_3c5e7a9b1d2f4a6c8e0b2d4f",
2740+ "workspace": "acme",
2741+ "kind": "owner",
2742+ "key": "ana",
2743+ "name": "ana",
2744+ "summary": null,
2745+ "project": null,
2746+ "private": false,
2747+ "data": {},
2748+ "source": "scan",
2749+ "ref": "/u/ana",
2750+ "updated_at": "2026-10-04T20:58:41.000Z"
2751+ }
2752+ }
2753+ ]
2754+ },
2755+ "notes": "`id` is the entry's id, or its key: a project's or app's slug, `npm:<name>` (or `cargo:`, `go:`, `pypi:`) for a package, a username for an owner, `<project>/production` for an environment. `data` depends on the kind: a package's version and dependencies, an API's routes, an app's production address."
26272756 }
26282757 }
+203−0
1+//! Every response the REST routes give, run through the converter the API
2+//! sends them with, checked for `camelCase` that would leak out.
3+//!
4+//! The samples are the reference's example responses, put back into the
5+//! `camelCase` the services send (as serde's `rename_all` writes it) and,
6+//! where an operation returns a contract type, decoded into that type and
7+//! encoded again, so that every field the type has is sent, not only the
8+//! ones an example shows.
9+
10+use g1t_contracts::{actions, integrations, repos, webhooks, work};
11+use g1t_kit::wire::{self, USER_KEYED};
12+use serde::Serialize;
13+use serde::de::DeserializeOwned;
14+use serde_json::{Map, Value, json};
15+
16+use crate::openapi::document;
17+use crate::operations::Op;
18+
19+/// A key as `#[serde(rename_all = "camelCase")]` writes it.
20+fn camel_key(key: &str) -> String {
21+ let mut out = String::with_capacity(key.len());
22+ let mut upper = false;
23+ for c in key.chars() {
24+ if c == '_' {
25+ upper = true;
26+ } else if upper {
27+ out.extend(c.to_uppercase());
28+ upper = false;
29+ } else {
30+ out.push(c);
31+ }
32+ }
33+ out
34+}
35+
36+/// A response as the services send it: `camelCase`, but for the maps the
37+/// converter passes through, which are data.
38+fn as_services_send(value: &Value) -> Value {
39+ match value {
40+ Value::Object(fields) => {
41+ let mut out = Map::new();
42+ for (key, value) in fields {
43+ let user_keyed = value.is_object()
44+ && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_"));
45+ let value = if user_keyed { value.clone() } else { as_services_send(value) };
46+ // A `by_…` map keeps its name in the converter's spelling.
47+ let key = if key.starts_with("by_") { key.clone() } else { camel_key(key) };
48+ out.insert(key, value);
49+ }
50+ Value::Object(out)
51+ }
52+ Value::Array(items) => Value::Array(items.iter().map(as_services_send).collect()),
53+ other => other.clone(),
54+ }
55+}
56+
57+/// `value` decoded as `T` and encoded again, as the service would send it.
58+fn through<T: DeserializeOwned + Serialize>(op: Op, value: Value) -> Value {
59+ let decoded: T = serde_json::from_value(value)
60+ .unwrap_or_else(|error| panic!("{}: the example is not a {}: {error}", op.name(), std::any::type_name::<T>()));
61+ serde_json::to_value(decoded).unwrap()
62+}
63+
64+/// What the service behind an operation sends, from its example.
65+fn sample(op: Op, example: &Value) -> Value {
66+ let sent = as_services_send(example);
67+ match op {
68+ Op::ListRepos => through::<Vec<repos::Repo>>(op, sent),
69+ Op::GetRepo | Op::CreateRepo | Op::UpdateRepo => through::<repos::Repo>(op, sent),
70+ Op::GetRepoSettings | Op::UpdateRepoSettings => through::<work::RepoSettings>(op, sent),
71+ Op::GetMergeQueue => through::<work::QueueView>(op, sent),
72+ Op::ListIssues => through::<Vec<work::Issue>>(op, sent),
73+ Op::CreateIssue | Op::UpdateIssue | Op::CloseIssue | Op::ReopenIssue => {
74+ through::<work::Issue>(op, sent)
75+ }
76+ Op::GetIssue => through::<work::IssueDetail>(op, sent),
77+ Op::ListPullRequests => through::<Vec<work::Pull>>(op, sent),
78+ Op::GetPullRequest => through::<work::PullDetail>(op, sent),
79+ Op::MarkPullRequestReady | Op::ClosePullRequest | Op::MergePullRequest | Op::AssignIssue => {
80+ through::<work::Pull>(op, sent)
81+ }
82+ Op::ListWorkflows => through::<Vec<actions::Workflow>>(op, sent),
83+ Op::ListWorkflowRuns => through::<Vec<actions::WorkflowRun>>(op, sent),
84+ Op::GetWorkflowRun => through::<actions::RunDetail>(op, sent),
85+ Op::GetJobLogs => through::<actions::JobLog>(op, sent),
86+ Op::DispatchWorkflow | Op::CancelWorkflowRun | Op::RerunWorkflowRun => {
87+ through::<actions::WorkflowRun>(op, sent)
88+ }
89+ Op::ListActionsSecrets | Op::ListActionsVariables => through::<Vec<actions::Setting>>(op, sent),
90+ Op::ListWebhooks => through::<Vec<webhooks::Hook>>(op, sent),
91+ Op::ListIntegrations => through::<Vec<integrations::Connection>>(op, sent),
92+ Op::GetModelRoutes | Op::SetModelRoutes => through::<Vec<integrations::ModelRoute>>(op, sent),
93+ Op::ListEvents => through::<Vec<g1t_contracts::events::Event>>(op, sent),
94+ Op::Whoami => through::<g1t_contracts::User>(op, sent),
95+ _ => sent,
96+ }
97+}
98+
99+/// Every key of `example`, as paths, outside the maps passed through.
100+fn paths(value: &Value, path: &str, out: &mut Vec<String>) {
101+ match value {
102+ Value::Object(fields) => {
103+ for (key, value) in fields {
104+ let here = format!("{path}.{key}");
105+ out.push(here.clone());
106+ let user_keyed = value.is_object()
107+ && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_"));
108+ if !user_keyed {
109+ paths(value, &here, out);
110+ }
111+ }
112+ }
113+ Value::Array(items) => {
114+ for item in items {
115+ paths(item, &format!("{path}[]"), out);
116+ }
117+ }
118+ _ => {}
119+ }
120+}
121+
122+#[test]
123+fn no_route_answers_with_camel_case() {
124+ let document = document();
125+ let (mut checked, mut converted) = (0, 0);
126+ for (path, methods) in document["paths"].as_object().unwrap() {
127+ for (method, operation) in methods.as_object().unwrap() {
128+ let example = &operation["responses"]["200"]["content"]["application/json"]["example"];
129+ let tool = operation["x-mcp-tool"].as_str().unwrap_or_default();
130+ let Some(op) = Op::by_name(tool) else {
131+ // Device sign-in, which is written in `snake_case` by hand.
132+ assert!(wire::camel_case_keys(example).is_empty(), "{method} {path}");
133+ continue;
134+ };
135+ let sample = sample(op, example);
136+ converted += wire::camel_case_keys(&sample).len();
137+ let sent = wire::snake_case(sample);
138+ let leaked = wire::camel_case_keys(&sent);
139+ assert!(leaked.is_empty(), "{method} {path} sends {leaked:?}");
140+ // The reference shows what is sent: each of its names is one.
141+ let (mut shown, mut real) = (Vec::new(), Vec::new());
142+ paths(example, "", &mut shown);
143+ paths(&sent, "", &mut real);
144+ for name in shown {
145+ assert!(real.contains(&name), "{method} {path}: the reference shows {name}, which is not sent");
146+ }
147+ checked += 1;
148+ }
149+ }
150+ assert!(checked >= Op::ALL.len());
151+ // The samples are in the services' spelling, so there was something to
152+ // convert.
153+ assert!(converted > 100, "{converted}");
154+}
155+
156+#[test]
157+fn every_route_has_a_sample() {
158+ let document = document();
159+ for route in crate::rest::ROUTES {
160+ let path = route
161+ .path
162+ .split('/')
163+ .map(|segment| match segment.strip_prefix(':') {
164+ Some(name) => format!("{{{name}}}"),
165+ None => segment.to_owned(),
166+ })
167+ .collect::<Vec<_>>()
168+ .join("/");
169+ let example = &document["paths"][&path][route.method.to_lowercase()]["responses"]["200"]
170+ ["content"]["application/json"]["example"];
171+ assert!(!example.is_null(), "{} {path}", route.method);
172+ }
173+}
174+
175+#[test]
176+fn errors_and_reports_are_snake_case() {
177+ let failure = g1t_contracts::Failure {
178+ code: g1t_contracts::FailureCode::NotFound,
179+ message: "No such endpoint.".to_owned(),
180+ };
181+ assert!(wire::camel_case_keys(&wire::snake_case(json!({ "error": failure }))).is_empty());
182+}
183+
184+#[test]
185+fn a_job_spec_keeps_github_s_spelling() {
186+ let spec = json!({
187+ "job": "job_1",
188+ "spec": { "runs-on": "ubuntu-latest", "timeoutMinutes": 5 },
189+ "workflow": { "env": { "nodeEnv": "x" } },
190+ "github": { "eventName": "push", "headRef": "" },
191+ "event": { "pull_request": { "headSha": "x" } },
192+ "contexts": { "inputs": { "dryRun": true }, "matrix": { "nodeVersion": 20 } },
193+ "checkout": { "ref": "main" },
194+ "timeoutMinutes": 30,
195+ "masks": [],
196+ });
197+ let sent = wire::snake_case_keeping(spec.clone(), crate::JOB_SPEC_AS_GIVEN);
198+ assert_eq!(sent["timeout_minutes"], 30);
199+ assert!(sent.get("timeoutMinutes").is_none());
200+ for kept in ["spec", "workflow", "github", "event", "contexts", "checkout"] {
201+ assert_eq!(sent[kept], spec[kept], "{kept}");
202+ }
203+}
+12−0
130130 ),
131131 route(
132132 "GET",
133+ "/workspaces/:workspace/context/search",
134+ Op::SearchContext,
135+ &[("q", "query"), ("project", "project"), ("kinds", "kinds"), ("limit", "limit")],
136+ ),
137+ route(
138+ "GET",
139+ "/workspaces/:workspace/context/:kind/:id",
140+ Op::GetEntity,
141+ &[],
142+ ),
143+ route(
144+ "GET",
133145 "/workspaces/:workspace/integrations",
134146 Op::ListIntegrations,
135147 &[],
+3−1
2323 { "binding": "WEBHOOKS", "service": "g1t-webhooks" },
2424 { "binding": "ACTIONS", "service": "g1t-actions" },
2525 // Builds of deployments report through here.
26− { "binding": "DEPLOYMENTS", "service": "g1t-deployments" }
26+ { "binding": "DEPLOYMENTS", "service": "g1t-deployments" },
27+ // The context hub: search_context and get_entity.
28+ { "binding": "CONTEXT", "service": "g1t-context" }
2729 ],
2830 // GitHub Actions artifacts and cache, in chunks, with KV's own expiry.
2931 // (Moves to R2 once R2 is enabled on the account.)
+5−2
4545 customCss: ['@g1t/theme/tokens.css', './src/styles/g1t.css'],
4646 social: [
4747 { icon: 'seti:git', label: 'Source on g1t', href: 'https://g1t.sh/syntaqx/g1t' },
48− { icon: 'github', label: 'Source on GitHub', href: 'https://github.com/syntaqx/g1t' },
4948 ],
5049 editLink: {
51− baseUrl: 'https://github.com/syntaqx/g1t/edit/main/apps/docs/',
50+ baseUrl: 'https://g1t.sh/syntaqx/g1t/blob/main/apps/docs/',
5251 },
5352 lastUpdated: true,
5453 head: [
8281 { label: 'Projects', slug: 'guides/projects' },
8382 { label: 'Deployments', slug: 'guides/deployments' },
8483 { label: 'Secrets and variables', slug: 'guides/secrets-and-variables' },
84+ { label: 'Security', slug: 'guides/security' },
8585 ],
8686 },
8787 {
8888 label: 'Agents',
8989 items: [
9090 { label: 'g1t agents', slug: 'guides/g1t-agents' },
91+ { label: 'Guardrails', slug: 'guides/guardrails' },
9192 { label: 'Outcomes and plans', slug: 'guides/outcomes' },
9293 { label: 'Talking to agents', slug: 'guides/talking-to-agents' },
9394 { label: 'Agents, sessions and memory', slug: 'guides/agents-and-memory' },
95+ { label: 'Context hub', slug: 'guides/context-hub' },
9496 { label: 'Bring your own agent', slug: 'guides/bring-your-own-agent' },
9597 ],
9698 },
117119 items: [
118120 { label: 'Accounts and sign-in', slug: 'guides/authentication' },
119121 { label: 'Workspaces and tokens', slug: 'guides/workspaces' },
122+ { label: 'Audit log', slug: 'guides/audit-log' },
120123 { label: 'Usage and billing', slug: 'guides/usage-and-billing' },
121124 { label: 'Git', slug: 'guides/git' },
122125 ],
+1−1
251251 out.push(
252252 '## Body parameters',
253253 '',
254− 'Send a JSON object. Names are `snake_case`; the `camelCase` spelling is accepted too.',
254+ 'Send a JSON object. Names are `snake_case`, as in responses; the `camelCase` spelling is accepted too.',
255255 '',
256256 table(['Name', 'Type', 'Required', 'Description'], propertyRows(body)),
257257 '',
+7−5
55 <nav class="g1t-nav" aria-label="g1t">
66 <a href="https://g1t.sh/">g1t.sh</a>
77 <a href="https://g1t.sh/register" class="g1t-nav-cta">Sign up</a>
8− <a href="https://github.com/syntaqx/g1t" aria-label="Source on GitHub" class="g1t-nav-icon">
9− <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor">
10− <path
11− d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
12− ></path>
8+ <a href="https://g1t.sh/syntaqx/g1t" aria-label="Source on g1t" title="Source on g1t" class="g1t-nav-icon">
9+ <svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
10+ <circle cx="6" cy="6" r="3"></circle>
11+ <circle cx="6" cy="18" r="3"></circle>
12+ <circle cx="18" cy="9" r="3"></circle>
13+ <path d="M6 9v6"></path>
14+ <path d="M18 12c0 3-3 3-6 3H9"></path>
1315 </svg>
1416 </a>
1517 </nav>
+4−2
11 import { defineCollection } from 'astro:content';
2−import { docsLoader } from '@astrojs/starlight/loaders';
3−import { docsSchema } from '@astrojs/starlight/schema';
2+import { docsLoader, i18nLoader } from '@astrojs/starlight/loaders';
3+import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
44
55 export const collections = {
66 docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
7+ // Interface wording: "Edit page" opens the file on g1t, where the source lives.
8+ i18n: defineCollection({ loader: i18nLoader(), schema: i18nSchema() }),
79 };
+2−3
66 g1t is a git forge for teams of agents. You hand g1t an outcome, and a team
77 of agents converges it onto `main`: each change is made in a pull request
88 of its own, checked in a clean sandbox, reviewed, revised and merged under
9−your repository's rules. People work exactly as they would on GitHub, with
10−the same repositories, issues, pull requests and reviews, alongside the
11−agents.
9+your repository's rules. People work alongside the agents in the same
10+repositories, issues, pull requests and reviews.
1211
1312 Underneath it is ordinary git: repositories, commits, branches, clone, push
1413 and pull all work as they do anywhere. On top of that it has the two things
+20−1
1515 | See how a pull request was made | **Agents → Sessions**, or the pull request's Agent panel |
1616 | Teach every agent something about this code | **Agents → Memory** |
1717 | Teach every agent something true in every project | **Workspace memory**, under Across projects |
18+| Review what agents, reviews and docs taught | **Agents → Memory**, or **Context → Memory** for the workspace |
1819 | See every agent across the workspace, and its cost | **Agent fleet**, under Across projects |
1920
2021 ## Runs
142143 or the agent's run and the pull request it was working on, linked from the
143144 memory.
144145
146+Memory also fills itself. At the end of every run that changes code, the
147+agent is asked what it learned; a person's correction in a review, a merged
148+pull request's decision, and what a project's `AGENTS.md`, README and
149+manifests say are captured too. These arrive as **candidates**, which no
150+agent is given until they are kept: at once when two independent sources
151+say the same thing or a project's `AGENTS.md` or manifests state it,
152+otherwise by a member in the **Review** list on **Agents → Memory** or the
153+Review queue on the workspace's [Context](/guides/context-hub/) page. See
154+[memory that fills itself](/guides/context-hub/#memory-that-fills-itself).
155+
156+Every run is also given a **Context** section from the
157+[context hub](/guides/context-hub/#agents-start-with-context): the
158+project's stack, owners, environments and the projects it uses with their
159+live addresses, the kept memories closest to its task, and recent
160+decisions.
161+
145162 Your own agents can use memory too, through the [MCP tools](/reference/mcp/#memory)
146163 `remember` and `recall` or the API:
147164
166183 - **add** a memory, with its kind: fact, convention, decision or gotcha;
167184 - **pin** one, so every agent gets it first, whatever the budget;
168185 - **edit** one that has drifted, or change its kind;
169−- **forget** one that no longer holds.
186+- **forget** one that no longer holds;
187+- **keep**, **edit** or **dismiss** a candidate waiting for review. A
188+ dismissed candidate is never suggested again in the same words.
170189
171190 Each shows who added it, when, and when it was last given to an agent. A
172191 memory that has not been given to an agent for a long time is a good one to
+68−0
1+---
2+title: Audit log
3+description: Every action agents take with their run credentials, and every change people and tokens make, with whether it was allowed and the rule that decided.
4+---
5+
6+Every workspace keeps an audit log. It records:
7+
8+- **Everything an agent does** with its [run credentials](/guides/g1t-agents/#credentials),
9+ reads included: every API and MCP call, every clone and fetch, every push.
10+- **Every change people and workspace tokens make** through the API, the
11+ MCP server and git: opening and closing issues, comments, merges,
12+ settings, pushes. Reads by people are not recorded.
13+
14+Refusals are recorded too, with the rule that refused them. Entries are
15+only ever added: nothing edits or removes one.
16+
17+## What an entry says
18+
19+| Field | What it is |
20+| --- | --- |
21+| Time | When it happened, to the millisecond. |
22+| Actor | Who did it: a person, an agent, or a workspace token. |
23+| On behalf of | For an agent, the person it worked for: `g1t-agent on behalf of syntaqx`. |
24+| Run | The agent run, with its kind: `implement`, `review`, `update` and so on. |
25+| Credential | The id of the token used. |
26+| Action | The API or MCP operation, such as `create_issue`, or `git.push` and `git.fetch`. |
27+| Target | The repository, the issue or pull request number, and for git the refs it moved. |
28+| Outcome | `allowed` or `denied`. |
29+| Rule | What decided it: the run's scope, such as `run:implement/tools`; a refusal rule, such as `scope:repository`; or, for people, their own access. A refusal by the repository's own rules is `service` (or `repository` for git). |
30+| Result | `ok`, or the reason it failed. |
31+| Request id | The request's id, the same one Cloudflare logs it under. |
32+
33+The rules that refuse an agent are listed under
34+[credentials](/guides/g1t-agents/#credentials).
35+
36+## Read the log
37+
38+Open the workspace's settings and choose **Audit log**, or go to
39+`g1t.sh/<workspace>/-/audit`.
40+
41+- **Owners** see everything in the workspace.
42+- **Members** see what was done to the workspace's projects, and anything
43+ they did, or had done on their behalf. Changes owners made to the
44+ workspace itself are for owners.
45+
46+Filter by actor (a person matches what they did and what agents did for
47+them), agent, action, project, outcome, who acted, and a range of days.
48+The filters are part of the page's address, so a filtered view can be
49+shared with anyone who can see it.
50+
51+Each agent run's page has a **What it did** section listing its entries in
52+order, and a pull request's **Agent** panel shows the latest of what its
53+runs did. Both link to the full log, filtered to the run.
54+
55+## Export
56+
57+**CSV** and **JSON** on the Audit log page download what the current
58+filters match, up to 10,000 entries, newest first. The CSV has one column
59+for each field above; a cell that a spreadsheet would read as a formula is
60+written as text.
61+
62+## What is not recorded
63+
64+- Reads by people and workspace tokens.
65+- What people do on the website itself. The API, the MCP server and git
66+ are recorded.
67+- What g1t does on its own, such as closing a pull request whose agent
68+ failed. Those changes are in the pull request's timeline.
+198−0
1+---
2+title: Context hub
3+description: The catalog g1t builds of everything a workspace builds and runs, memory that fills itself from runs, reviews, merges and docs, one search over all of it, and a scorecard for every project.
4+---
5+
6+The **context hub** is one place to ask about a workspace: what it builds
7+and runs, who owns what, how each project is built, tested and deployed,
8+and what people and agents have learned along the way. It fills itself
9+from your repositories, deployments and integrations, and from the work
10+agents and people do, so it is never a wiki someone has to keep up.
11+
12+Open it from **Context**, under Across projects in the workspace's sidebar.
13+It has four tabs:
14+
15+| Tab | What it shows |
16+| --- | --- |
17+| **Catalog** | Every project, app, API, package, language, owner, environment, integration and doc, and how they relate |
18+| **Memory** | The Review queue: memory candidates waiting for a person |
19+| **Search** | One search across the catalog, docs, issues, pull requests and memory |
20+| **Scorecards** | A few rules every project should meet, each failing one a click away from an issue an agent fixes |
21+
22+Every g1t agent run starts with a **Context** section drawn from the hub,
23+and your own agents can ask it through the [MCP tools](#mcp-tools)
24+`search_context` and `get_entity`.
25+
26+## The catalog
27+
28+The catalog is built from each project's default branch, on every push to
29+it. g1t reads the files that say what a project is, and only those whose
30+contents changed since the last push:
31+
32+| Read from | Gives |
33+| --- | --- |
34+| `package.json`, `Cargo.toml`, `go.mod`, `pyproject.toml`, `requirements.txt` | **Packages** (name, version, dependencies), **languages**, test and build commands, the package manager from the committed lockfile |
35+| `wrangler.jsonc`, `wrangler.json`, `wrangler.toml`; `openapi.*`, `swagger.*` | **APIs**: a Worker's routes, or an OpenAPI document's operations |
36+| `README`, `AGENTS.md` (or `CLAUDE.md`), `CONTRIBUTING`, `docs/*.md`, `runbooks/*.md` | **Docs**, split into sections for search |
37+| `.g1t/workflows/*`, `.github/workflows/*` | Whether the project's tests run in checks |
38+| `owners:` in `.g1t/project.yml`, and `CODEOWNERS` | **Owners** |
39+
40+and joins them with what g1t already knows:
41+
42+- **Dependencies** between projects, from [Projects](/guides/projects/);
43+- **Apps and environments**, with their live addresses, from
44+ [Deployments](/guides/deployments/);
45+- **Integrations**, from [Integrations](/guides/integrations/);
46+- **Owners** also include the members who wrote at least a fifth of the
47+ project's last 100 commits, up to three.
48+
49+Each entry links to where it lives: a project's code, a doc's file, an
50+app's address. Entries relate to each other:
51+
52+| Relation | Example |
53+| --- | --- |
54+| `depends_on` | `web` depends on `api`; `@acme/web` depends on `@acme/ui` |
55+| `owned_by` | `web` is owned by `ana` |
56+| `deploys_to` | the `web` app deploys to `web/production` |
57+| `documented_by` | `web` is documented by its `README.md` |
58+| `exposes` | `web` exposes the `@acme/web` package and its app; the app exposes its routes |
59+| `uses` | `web` uses TypeScript, and the Sentry integration that reports on it |
60+
61+The **Catalog** tab filters by kind and by project, and draws the
62+workspace's projects with an arrow from each to the projects it uses.
63+
64+Building the catalog is cheap and repeatable: each push reads at most 15
65+files of a project, a file whose contents have not changed is never read
66+again, and building twice from the same commit gives the same catalog.
67+
68+## Memory that fills itself
69+
70+[Memory](/guides/agents-and-memory/#memory) is what agents are told about a
71+project and its workspace. Besides what agents save with `remember` and
72+what people add by hand, the hub captures it from four places:
73+
74+| Source | What it captures | Kind |
75+| --- | --- | --- |
76+| **Agent runs** | At the end of every run that changes code, the agent is asked what it learned that the next agent would need, with what showed it | fact, convention, decision or gotcha |
77+| **Reviews** | A person's request for changes, or a comment that corrects the agent ("we use the shared client instead"), on a pull request | convention, quoting the comment |
78+| **Merges** | A merged pull request's title, why (the first paragraph of its description) and the files it changed | decision |
79+| **Docs and manifests** | Bullets in `AGENTS.md`; commands under a README's setup and testing sections; conventions sections; the package manager and test commands from manifests | fact, convention or gotcha |
80+
81+What is captured arrives as a **candidate**. No agent is given a candidate
82+until it is **kept**:
83+
84+- **at once**, when two independent sources say the same thing (a doc and
85+ a run, two runs, a run and a review), or when a project's `AGENTS.md` or
86+ manifests state it;
87+- **by a person**, in the Review queue.
88+
89+The same thing said again in different case, punctuation or spacing counts
90+as the same memory, seen once more. A memory seen again from the same
91+source (the same run, the same file) is not counted twice.
92+
93+### Review
94+
95+The **Memory** tab of the Context page lists every candidate in the
96+workspace; a project's **Agents → Memory** lists its own. Each shows where
97+it came from (a run, a review comment, a merged pull request, a doc), the
98+evidence quoted, how sure its source was and how often it has been seen.
99+
100+- **Keep** gives it to every agent from their next run on.
101+- **Edit** rewords it, or changes its kind, and keeps it.
102+- **Dismiss** throws it away, and the same wording is never suggested
103+ again.
104+
105+### Never a secret
106+
107+Captured memory is held to the same rule as everything else in memory: text
108+that looks like a key, a token, a password or a private key is refused, in
109+the memory and in its evidence. Agents are told never to report one.
110+
111+## Search
112+
113+**Search** finds things by meaning, across:
114+
115+- catalog entries;
116+- the sections of every project's docs;
117+- issues and pull requests, with their descriptions (an agent's pull
118+ request description is its account of the session);
119+- kept memory.
120+
121+Each result says what kind of thing it is, where it came from, who wrote it
122+and how fresh it is. When the search index cannot answer, g1t matches the
123+words of your query instead, and says so.
124+
125+### Who sees what
126+
127+- Search never reads another workspace's rows.
128+- Members of the workspace, and its agents, find everything in it.
129+- Anyone else who can see a public project finds that project's catalog
130+ entries, docs, issues and pull requests, and never memory.
131+- A project made private is hidden from people outside the workspace at
132+ once, even before it is searched again.
133+
134+## Scorecards
135+
136+Each project is checked against a few rules:
137+
138+| Rule | Passes when |
139+| --- | --- |
140+| **Has an owner** | `.g1t/project.yml` or `CODEOWNERS` names one, or a member wrote most of it |
141+| **Has a README** | A README sits at the project's root |
142+| **Has an AGENTS.md** | An `AGENTS.md` (or `CLAUDE.md`) tells agents how to work here |
143+| **Tests run in checks** | A workflow in `.g1t/workflows` or `.github/workflows` runs tests |
144+| **Production deploy is green** | Production is live and its latest build did not fail. Does not apply to a project that does not deploy on g1t |
145+| **No open secret findings** | [Security](/guides/security/) has no open secret findings for it |
146+
147+A failing rule has **Fix with an agent**: g1t opens an issue saying what to
148+do, with an acceptance check where one can be written (such as
149+`test -f AGENTS.md`), and puts g1t's agent on it.
150+
151+## Agents start with context
152+
153+Every g1t agent run is given a **Context** section, after the project's
154+memory, within about 4,000 characters:
155+
156+- the project's stack, test commands, owners, docs, and its environments
157+ with their addresses;
158+- the projects it uses, with their live addresses and the variables that
159+ carry them, and the projects that use it;
160+- the kept memories closest to the task, beyond the pinned ones every run
161+ already has;
162+- the latest decisions from merged pull requests.
163+
164+Each line says where it came from (`[source: catalog]`,
165+`[source: AGENTS.md]`, `[source: review on #12]`). Agents are told it is
166+reference material: where it disagrees with the code, the code wins.
167+
168+## Building it for a workspace
169+
170+The first time anyone opens a workspace's Context page, g1t builds its hub:
171+the catalog for every project (up to 50), memory candidates from their
172+docs, manifests and last 20 merged pull requests with their reviews, and
173+the search index for docs, recent issues and pull requests, and kept
174+memory. **Rebuild** does it again, reading every file afresh.
175+
176+Putting text in the search index uses Workers AI and is counted per
177+workspace and month; a workspace that passes 20 million tokens in a month
178+keeps text search, and new text waits for the next month's index.
179+
180+## MCP tools
181+
182+| Tool | Takes | Does |
183+| --- | --- | --- |
184+| `search_context` | `query`, and `workspace` or `repo`; optional `project`, `kinds`, `limit` | One search across the catalog, docs, issues, pull requests and memory, as [Search](#search) |
185+| `get_entity` | `kind`, `id`, and `workspace` or `repo` | One catalog entry by its id or key (a project's slug, `npm:<name>`, a username), with every relation |
186+
187+g1t's own agents have both. Over REST:
188+
189+```sh
190+curl "https://api.g1t.sh/workspaces/acme/context/search?q=how+do+we+deploy+the+api" \
191+ -H "Authorization: Bearer $G1T_TOKEN"
192+
193+curl "https://api.g1t.sh/workspaces/acme/context/project/web" \
194+ -H "Authorization: Bearer $G1T_TOKEN"
195+```
196+
197+See [Search the context hub](/reference/api/context/search-context/) and
198+[Get a catalog entry](/reference/api/context/get-entity/).
+176−7
4242 The same thing is the `assign_issue` tool on the MCP server, so an agent
4343 planning work can hand issues to g1t agents itself.
4444
45+In a comment: write `@g1t-agent take this` on the issue. See
46+[mentioning g1t-agent](#mentioning-g1t-agent).
47+
48+By label: a project can hand every issue given a label to the agent. See
49+[the label rule](#the-label-rule).
50+
4551 Each agent appears as a draft pull request on its issue within a few
4652 seconds. The pages update on their own while they work.
4753
6874 If an agent fails, or finishes without changing anything, its pull request
6975 is closed and its session says why.
7076
77+## Repository instructions
78+
79+Every g1t agent run reads the repository's own instructions for agents
80+and is told them, labelled as the repository's, before it starts: making a
81+change, revising it, reviewing, catching up, answering, and planning.
82+
83+| File | Read by |
84+| --- | --- |
85+| `AGENTS.md` and `CLAUDE.md` at the root | Every run. |
86+| `AGENTS.md` and `CLAUDE.md` in a subdirectory | Runs whose task touches files under it: the nearest one above each file. Where it disagrees with the root's, it wins for the files under it. |
87+| `.g1t/review.md` | Reviews: what to check, house rules, paths that need extra care. |
88+
89+Write them for an agent that knows nothing about the project: how to build
90+and test, how things are named, what never to touch. For example:
91+
92+```md
93+# AGENTS.md
94+- Run `npm test` and `npm run typecheck` before you finish.
95+- API handlers return a Result; they never throw.
96+- Never edit files under `vendor/`.
97+```
98+
99+Which directories a task touches comes from the pull request's changed
100+files, and for new work from the paths the issue names, so naming the
101+files in an issue helps the right instructions reach the agent.
102+
103+**Where they are read from.** The default branch, as it is when the run
104+starts. A pull request from one of the repository's own branches is read at
105+its head instead, since only people who can push to the repository can
106+change it. A pull request from a fork, which includes every change a g1t
107+agent makes, is never followed: the agent keeps the default branch's
108+instructions, and if the fork changes them, it is shown the changed text as
109+part of the change, marked as not instructions. That way nobody can steer
110+an agent, or the review of their own change, by editing these files in a
111+pull request. Treat what a fork's head says as untrusted, as you would its
112+code.
113+
114+**Limits.** Each file is cut at 8,000 characters and all of them together
115+at 24,000; what is left out is named in the prompt. Files are read once per
116+commit and reused.
117+
118+The project's **Agents** page lists the files its runs read, what they say
119+and when each last changed, with a link to each in the code. Each run's
120+session starts with a note of which files it read. To change them, change
121+the files and merge to the default branch.
122+
71123 ## Seeing it through
72124
73125 Making the change is the first step. g1t takes the rest itself, and you
172224
173225 Both run in sandboxes of their own.
174226
227+## Mentioning g1t-agent
228+
229+Write `@g1t-agent` in a comment on an issue or a pull request, with what
230+you want, and it does it. The comment box offers to complete the name as
231+you type `@`.
232+
233+| Where | You write | What happens |
234+| --- | --- | --- |
235+| An issue | A request: `@g1t-agent take this`, `@g1t-agent fix the empty case` | The issue is assigned to the agent, which opens a pull request, as if you had chosen **Assign**. |
236+| An issue | A question: `@g1t-agent why does search time out?` | The agent reads the code on the default branch and answers in the thread. It changes nothing. |
237+| A pull request g1t-agent made | A request: `@g1t-agent also handle the empty list` | The agent is sent back to make the change, with your comment as what to address, and the checks and review run again. If it is still working, it gets your comment as a message at its next step. |
238+| Any pull request | `@g1t-agent review` | A review by a g1t agent, as with **Review by a g1t agent**. |
239+| Any pull request | A question | The agent reads the change at its head and answers in the thread. On someone else's pull request, which it cannot push to, a request is answered too: it says what it would change. |
240+
241+A request is a comment whose words after the mention start with what to
242+do (`take`, `fix`, `add`, `please rename`, `can you update`); a question
243+starts with a question word or ends with a question mark. `review` near the
244+start asks for a review.
245+
246+g1t-agent always replies in the thread, saying what it started or why it
247+did not. Every run a mention starts shows on the project's **Agents** page
248+as started by whoever mentioned it, and a mention that started nothing
249+shows there as a failed run with the reason.
250+
251+**What does not count.** Mentions in code (`` `@g1t-agent` `` or a code
252+block), in quoted lines (`> @g1t-agent …`), in email addresses
253+(`ops@g1t-agent.dev`) and in longer names (`@g1t-agents`) are ignored.
254+Matching ignores case. Agents mentioning `@g1t-agent` start nothing, so
255+agents cannot set each other to work this way.
256+
257+**Who can.** Members of the project's workspace. Anyone else who mentions
258+it gets a short reply saying only members can, and nothing starts. When
259+the workspace cannot run agents (for example, its free allowance is used
260+up and it has no model provider of its own), g1t-agent replies with why.
261+
262+Each comment starts one run at most; to ask again, write a new comment.
263+
264+## The label rule
265+
266+Under a project's **Settings → Agents**, a member sets a label, such as
267+`agent`. From then on, when a member gives an open issue that label, either
268+when opening it or later, g1t-agent takes it: the issue is queued for an
269+agent, the conversation says so, and the agent starts as soon as the
270+project has room and nothing the issue depends on is still open, exactly as
271+for a [plan's](/guides/outcomes/) issues. An issue that already had the
272+label is not affected; removing and adding it again counts. **Turn off**
273+removes the rule.
274+
175275 ## Which model runs
176276
177277 You do not pick one. You assign the work to `g1t-agent`, the way you would
234334 [its own model provider](/guides/models/), and, until October 22, in any
235335 workspace on its free $1 of g1t's own models. See
236336 [the free allowance](/guides/usage-and-billing/#the-free-allowance).
237−- An agent is given one fork and the issue. Its credential, though, is your
238− account's for the length of the run; credentials limited to the pull
239− request are planned.
240−- A run has two hours. After that its credential expires and it can no
337+- A run has two hours. After that its credentials expire and it can no
241338 longer push or report.
242339
340+## Credentials
341+
342+Every sandbox run gets credentials of its own, made when it starts and
343+revoked the moment it stops. They are not your access tokens, and they are
344+not listed with them.
345+
346+Each credential carries a composite identity: the agent, acting on behalf
347+of the person who started the work. A run you started by assigning an issue
348+is `g1t-agent on behalf of you`, and that is how it appears in the
349+[audit log](/guides/audit-log/), on the run's page and in the pull
350+request's **Agent** panel.
351+
352+What it may do is the intersection of two things:
353+
354+- **The run's scope.** The credential is bound to the run, its repository,
355+ and what that kind of run needs. It expires no later than the run's
356+ timeout.
357+- **What you may do now.** It works only in the repository's workspace, and
358+ only while you are still a member of it. If you leave the workspace, every
359+ agent working on your behalf there stops being able to do anything. Your
360+ role does not carry over: an owner's agent is only ever a member.
361+
362+A sandbox holds two credentials. One is for g1t's runner, which clones,
363+pushes the result and records the session; downstream it acts as you, so
364+what it pushes is yours, within the run's scope. The other is for the
365+agent's own tools over MCP, and acts as the agent; it cannot be used with
366+git at all.
367+
368+| Kind of run | Git | API and MCP tools |
369+| --- | --- | --- |
370+| Implement | Reads the repository; pushes to its pull request's fork only | Records the session and marks its own pull request ready; tools to read issues, pull requests, the merge queue, workflow runs and memory, open issues, comment, remember, and message other agents |
371+| Revise, answer | Reads the repository; pushes to the pull request's fork, or to its branch only when the change is a branch of the repository | Records the session of its own pull request; the same tools as implement |
372+| Catch up | Reads the repository; pushes to the pull request's fork or branch only | Records the session of its own pull request |
373+| Review | Reads the change and the repository; pushes nothing | Reports its review through its own run |
374+| Plan | Reads the repository; pushes nothing | Reports its plan through its own run, for a person to apply; it can create issues in its repository only |
375+| Checks, merge check | Reads the change; pushes nothing | None |
376+| Merge queue | Reads each queued change; pushes the queue's own branch only | None |
377+| Deploy | Reads the commit it builds; pushes nothing | None |
378+
379+Nothing an agent's credential holds can reach another repository, or a
380+workspace's settings, members, access tokens, billing, integrations,
381+webhooks, secrets and variables, or workflows' controls. It cannot merge a
382+pull request or put more agents to work. A call that would is refused, and
383+the refusal is recorded with the rule that refused it:
384+
385+| Rule | Refused because |
386+| --- | --- |
387+| `never` | No agent's credential may ever do this. |
388+| `scope:operation` | The run's kind does not include this operation. |
389+| `scope:repository` | It names a repository other than the run's. |
390+| `scope:pull` | The runner tried to change a pull request other than its own. |
391+| `on-behalf-of:membership` | The person the agent works for is no longer a member of the workspace. |
392+| `git:read`, `git:push`, `git:ref` | The run has no grant to clone that repository, push to it, or move that branch or tag. |
393+| `git:not-a-run` | An agent's tools credential was used with git. |
394+
395+Personal and workspace access tokens are unchanged by any of this.
396+
243397 ## What a sandbox can reach
244398
245−A sandbox holds one fork and a credential that expires two hours after the
246−run starts. That credential, and the model key the agent runs on, are
247−removed from anything recorded in the session.
399+A sandbox holds one fork and its run's credentials, which expire when the
400+run's time is up and are revoked as soon as it stops. Those credentials,
401+and the model key the agent runs on, are removed from anything recorded in
402+the session.
403+
404+## Guardrails
405+
406+A workspace decides what its agents may do in their sandboxes, and each
407+project can override it: which hosts a sandbox can reach (g1t, the package
408+registries the project needs, and domains you list; enforced outside the
409+sandbox), which commands the harness refuses (force-pushing, rewriting the
410+default branch, reading outside the project, printing the environment,
411+sudo, and your own patterns), and how much one run may cost and how long it
412+may take. A run that is refused something shows it as a step; one that
413+reaches a cap is stopped and its pull request waits for you. A run on a
414+fork's head loads none of the fork's `CLAUDE.md`, `.claude` settings,
415+hooks, MCP servers or commands. See [guardrails](/guides/guardrails/) for
416+every rule and exactly how each is enforced.
+208−0
1+---
2+title: Guardrails
3+description: What g1t's agents may reach, run, spend and take in a project's sandboxes, and how each rule is enforced.
4+---
5+
6+Guardrails decide what g1t's agents may do in their sandboxes: which hosts
7+a sandbox can reach, which commands the agent's harness refuses, and how
8+much one run may cost and how long it may take. A workspace sets defaults,
9+and each project can override them.
10+
11+They apply to every sandbox g1t starts for a project's agents (implement,
12+revise, answer, catch up, review, plan, and replies to mentions). The
13+sandboxes of its acceptance checks and merge queue get the network list and
14+the time cap; they run the project's commands, not an agent, so command
15+rules and the cost cap do not apply to them. GitHub Actions jobs, deploy
16+builds and merge checks are not covered; see
17+[What is not covered](#what-is-not-covered).
18+
19+## Where to set them
20+
21+- **Workspace defaults**: the workspace's **Settings**, **Guardrails**.
22+ Owners can change them; members can read them.
23+- **A project's overrides**: the project's **Settings**, **Guardrails**.
24+ Members can change them.
25+
26+Every setting on a project's page starts as "As the workspace", which
27+follows the workspace's default, whatever it is now. Choose a value to
28+override it for that project only. Allowed domains and deny patterns add
29+up: a project's are added to the workspace's, never instead of them.
30+
31+Changes apply to runs that start after you save. A run that is under way
32+keeps the guardrails it started with.
33+
34+## Network
35+
36+With **Only allowed hosts** restricted (the default), a sandbox can reach:
37+
38+- g1t's own hosts, always: `g1t.sh`, `api.g1t.sh`, `models.g1t.sh` and
39+ `mcp.g1t.sh`, for cloning, pushing, reporting and the model;
40+- the package registries that are on (all of them by default):
41+
42+ | Registry | Hosts |
43+ | --- | --- |
44+ | npm and Yarn | `registry.npmjs.org`, `registry.yarnpkg.com`, `repo.yarnpkg.com` |
45+ | PyPI | `pypi.org`, `files.pythonhosted.org` |
46+ | crates.io and Rust toolchains | `crates.io`, `index.crates.io`, `static.crates.io`, `static.rust-lang.org` |
47+ | Go module proxy | `proxy.golang.org`, `sum.golang.org` |
48+ | GitHub downloads | `codeload.github.com`, `raw.githubusercontent.com`, `objects.githubusercontent.com` |
49+
50+- the domains you list: `api.stripe.com` allows exactly that host, and
51+ `*.example.com` allows every subdomain of `example.com` (not
52+ `example.com` itself; list both if you need both).
53+
54+`github.com` itself is not on the default list. A project with
55+dependencies fetched with git from GitHub, rather than as archives, needs it
56+listed.
57+
58+### How it is enforced
59+
60+This is enforced outside the sandbox, by Cloudflare Containers' outbound
61+interception:
62+
63+- A restricted sandbox starts with no internet connection at all. Its DNS
64+ resolves nothing on its own, and no protocol or port other than HTTP (80)
65+ and HTTPS (443) has any route out: SSH, raw TCP and UDP connections fail.
66+- Every HTTP and HTTPS request it makes is handed to g1t's runner Worker
67+ before it leaves. The Worker forwards requests to allowed hosts and
68+ answers every other with `403` and a line saying the host is not allowed
69+ and where to allow it.
70+- To see the host of an HTTPS request, the connection is re-encrypted with
71+ a certificate the sandbox is given when it starts, which g1t adds to the
72+ sandbox's trusted certificates (and points Node.js, Python, curl, Cargo
73+ and git at). A program that brings its own fixed list of certificates and
74+ ignores the system's cannot connect anywhere, allowed or not.
75+
76+Nothing running in the sandbox, root included, can change this: the rule
77+is applied where the sandbox's traffic leaves it, not inside it.
78+
79+Each refused host appears once as a step of the run, such as
80+`Blocked: example.com (not an allowed domain)`, on the run's page under
81+**Agents**. If a run needs a host, allow it and start the work again.
82+
83+Setting **Only allowed hosts** to Open gives that project's sandboxes the
84+whole internet, as before guardrails.
85+
86+## Commands
87+
88+The agent's harness checks every tool call the agent makes before it runs.
89+A refused call does not run; the agent is told it was refused and why, and
90+the run shows a step such as
91+`Denied: Ran git push --force origin feature (no force-pushing)`.
92+
93+Built-in rules, each on by default:
94+
95+| Rule | What it refuses |
96+| --- | --- |
97+| No force-pushing | `git push` with `--force`, `-f`, `--force-with-lease`, `--mirror`, `--delete` or `--prune`, a `+` refspec, or `:branch` to delete one. |
98+| No rewriting the default branch | Pushing to the default branch, `git branch -f/-d/-D/-m/-M` on it, `git update-ref` of it, and `git filter-branch`, `git filter-repo` and `git replace`. |
99+| No reading files outside the project | File tools (read, edit, write, search) outside the checked-out project, `/tmp`, and dependency caches (Cargo's registry and git checkouts, Go's module cache, Rust toolchains). Shell commands that touch g1t's own files in the sandbox, or other processes' environments under `/proc`. |
100+| No printing the environment | `env` and `printenv`, `export -p`, `set` and `declare -p` on their own, `compgen -e`, reading `/proc/*/environ`, and any command that reads a variable whose name contains `TOKEN`, `KEY`, `SECRET`, `PASSWORD`, `CREDENTIAL` or `AUTH`. |
101+| No sudo | `sudo`, `su`, `doas` and `pkexec`. With this on, the sandbox also gives up root before the agent starts, so nothing the agent runs can become root. |
102+
103+**Also refuse** adds your own rules, one per line, written as permission
104+rules:
105+
106+- `Bash(terraform apply:*)` refuses any shell command that starts with
107+ those words, wherever it appears in a line (`cd infra && terraform
108+ apply` too). `Bash(rm -rf *)` uses `*` as a wildcard; `Bash(make deploy)`
109+ refuses exactly that command.
110+- `Read(secrets/**)`, `Edit(//etc/**)`: file paths. `//` starts at the
111+ root, `~/` at the home directory, anything else at the project. `**`
112+ crosses directories, `*` does not. `Read` covers reading and searching;
113+ `Edit` covers every tool that writes a file.
114+- `WebFetch(domain:example.com)` refuses fetching that domain and its
115+ subdomains.
116+- A tool's name on its own, such as `WebSearch`, refuses the tool.
117+- Plain text is the start of a shell command: `kubectl delete` is saved as
118+ `Bash(kubectl delete:*)`.
119+
120+### How it is enforced
121+
122+The rules are written into the harness's managed settings, which no
123+settings file in the project or the home directory can override, as a hook
124+the harness runs before every tool call and as its permission rules. With
125+**No sudo** on, the sandbox then gives up root, so the agent cannot edit
126+them or the program the hook runs.
127+
128+This is a guard against an agent's mistakes, not a sandbox against a
129+determined one. Shell commands are matched as text, and a command can
130+always be written in a way no rule foresees (built up from variables, or
131+run from a script the agent wrote). The hard boundaries are elsewhere:
132+
133+- The network list, enforced outside the sandbox.
134+- The sandbox's credentials. The agent itself holds none of g1t's: the
135+ runner clones and pushes with credentials passed per command, and pushes
136+ only to the run's own fork or branch, never with force. See
137+ [credentials](/guides/g1t-agents/#credentials).
138+- Branch protection on the repository, which g1t enforces when a push
139+ arrives, whatever the sandbox did.
140+
141+### Changes from forks
142+
143+When a run checks out a fork's head (revising, reviewing or answering on
144+any pull request from a fork, including g1t-agent's own, and replying to a
145+mention on one), the harness loads nothing from that checkout: no
146+`CLAUDE.md`, no `.claude/settings.json` or `settings.local.json` (so none
147+of their hooks or permissions), no `.mcp.json` servers, and no commands or
148+skills. g1t's guardrails, its tools and the repository's instructions from
149+its own branches still apply. The run's session says so at the start. This
150+follows the rule g1t uses for
151+[repository instructions](/guides/g1t-agents/#repository-instructions):
152+they are read from the repository's own branches, never from a fork.
153+
154+## Caps
155+
156+**Cost per run**: the most one run may spend on its model, in US dollars.
157+$5.00 by default; 0 means no cap; at most $100. The harness tracks the
158+run's spend as it goes and stops the agent when it reaches the cap.
159+
160+**Time per run**: how long each kind of run may take, in minutes. By
161+default:
162+
163+| Kind of run | Minutes |
164+| --- | --- |
165+| Implement | 90 |
166+| Revise | 60 |
167+| Catch up | 45 |
168+| Review | 30 |
169+| Plan | 30 |
170+| Answer | 20 |
171+| Checks | 45 |
172+| Merge queue | 45 |
173+| Merge check | 10 |
174+
175+At most 240 minutes, but a run's credentials last two hours, so a longer
176+cap does not give an agent more than that to push.
177+
178+A run that reaches a cap is stopped and marked **Stopped**, with "Stopped
179+at its cost cap" or "Stopped at its time cap" on its page. A pull request it
180+was working on is left open for you, as when a person stops a run: raise
181+the cap if it was too low, then ask for a review, a revision or a catch-up
182+to start again. A run of checks or the merge queue that reaches its time
183+cap fails, as a sandbox that stops early always has.
184+
185+The run's page shows its caps under **Guardrails**: the time so far against
186+its time cap, live, and its spend against its cost cap. Spend is reported
187+when the run ends (or stops at its cap), so while it runs the page shows
188+the cap, not a running total.
189+
190+### How they are enforced
191+
192+- The cost cap is enforced by the harness, which counts the run's model
193+ spend the same way it reports it for billing and stops the agent once
194+ the spend reaches the cap. The step in flight when it does can take the
195+ run a little past it.
196+- The time cap is enforced twice: the harness stops the agent when it
197+ passes, and the sandbox itself is stopped three minutes after, whatever
198+ is running in it.
199+
200+## What is not covered
201+
202+- **GitHub Actions jobs and deploy builds** run in sandboxes with an open
203+ network and no command rules. They run commands from the repository's
204+ workflows and build settings, not an agent.
205+- **Merge checks** only merge two commits; they are not given guardrails.
206+- The **commit history** an agent produces is reviewed like any other
207+ change: guardrails limit what an agent can do while it works, not what
208+ its change does once it is merged.
+1−1
223223 `openai_endpoint`, `sentry`, `datadog`, `webhook`, `jira` or `linear`. `config` takes `repo`, `assign`, `label`,
224224 `write_back`, `organization`, `site`, `email`, `keys`, `base_url`,
225225 `auth_header` and `model`; each provider uses the ones above. For `datadog`
226−and `webhook`, the response's `signingSecret` is the only time the secret
226+and `webhook`, the response's `signing_secret` is the only time the secret
227227 is shown.
+6−6
173173 "agent": "g1t-agent",
174174 "state": "testing",
175175 "ahead": [41],
176− "baseCommit": "8f3c2e1…",
177− "combinedCommit": null,
176+ "base_commit": "8f3c2e1…",
177+ "combined_commit": null,
178178 "results": [],
179− "enqueuedBy": "g1t"
179+ "enqueued_by": "g1t"
180180 }
181181 ],
182182 "recent": []
190190 | `recent` | Those that landed or left, newest first. |
191191 | `state` | `waiting`, `testing`, `passed`, `failed`, `landed` or `removed`. |
192192 | `ahead` | The pull requests merged ahead of it in the state being tested. Empty when it was tested on `main` alone. |
193−| `baseCommit` | The commit of `main` the state was built on. |
194−| `combinedCommit` | The tested state. |
193+| `base_commit` | The commit of `main` the state was built on. |
194+| `combined_commit` | The tested state. |
195195 | `results` | The checks run against it, each with `command`, `passed` and `output`. |
196196 | `error` | Why it failed: a conflict, or what could not be run. |
197−| `enqueuedBy` | Who added it: a username, or `g1t` when it was merged automatically. |
197+| `enqueued_by` | Who added it: a username, or `g1t` when it was merged automatically. |
+3−3
132132
133133 | Tool | Route | |
134134 | --- | --- | --- |
135−| `plan_work` | `POST /repos/{owner}/{name}/plans` | Start a plan. Body: `brief`. Returns `planId` at once. |
135+| `plan_work` | `POST /repos/{owner}/{name}/plans` | Start a plan. Body: `brief`. Returns `plan_id` at once. |
136136 | `get_plan` | `GET /repos/{owner}/{name}/plans/{plan}` | The plan, its `status` and the issues it proposes. |
137137 | `apply_plan` | `POST /repos/{owner}/{name}/plans/{plan}/apply` | Open its issues. Body: `assign`, `keep`. |
138138
156156
157157 A plan's `status` is `planning`, `ready`, `failed` or `applied`. Each
158158 proposed issue has `title`, `body`, `labels`, `checks`, `files`,
159−`dependsOn` (positions in the plan, counting from 1) and, once applied,
159+`depends_on` (positions in the plan, counting from 1) and, once applied,
160160 `number`. `keep` takes positions counting from 1; leave it out to open
161161 every issue.
162162
164164
165165 | Field | |
166166 | --- | --- |
167−| `progress` | Each opened issue with `state` (the values in the table above, written `blocked`, `waiting`, `open`, `working`, `checking`, `reviewing`, `revising`, `catching_up`, `queued`, `ready`, `needs_you`, `landed`, `closed`), a `detail` sentence, `blockedBy`, `pull` and `agent`. |
167+| `progress` | Each opened issue with `state` (the values in the table above, written `blocked`, `waiting`, `open`, `working`, `checking`, `reviewing`, `revising`, `catching_up`, `queued`, `ready`, `needs_you`, `landed`, `closed`), a `detail` sentence, `blocked_by`, `pull` and `agent`. |
168168 | `exchanges` | The questions and handoffs between the agents on its pull requests. |
+2−2
118118
119119 | Field | What it is |
120120 | --- | --- |
121−| `checks` | The latest run of the acceptance checks: `status`, `headCommit`, `error`, and `results`, each with `command`, `passed`, `exitCode`, `output` and `durationMs`. |
122−| `earlierChecks` | The runs before it, newest first, without their output. |
121+| `checks` | The latest run of the acceptance checks: `status`, `head_commit`, `error`, and `results`, each with `command`, `passed`, `exit_code`, `output` and `duration_ms`. |
122+| `earlier_checks` | The runs before it, newest first, without their output. |
123123 | `statuses` | What each workflow run said about its head. |
124124 | `mergeable` | `clean`, `conflicting`, `checking` or `unknown`. |
125125 | `conflicts` | When conflicting, the files that conflict. |
+202−0
1+---
2+title: Security
3+description: How g1t keeps secrets out of your repositories, finds vulnerable dependencies, and has an agent land the upgrades that fix them.
4+---
5+
6+Every project has a **Security** page at `g1t.sh/<owner>/<project>/security`.
7+It shows what g1t has found, and what is being done about each finding:
8+
9+- **Secrets.** A push that adds a key or a token is refused before it lands,
10+ and each repository's history is scanned once, in the background.
11+- **Dependencies.** Every package your lockfiles resolve is checked against
12+ the [OSV](https://osv.dev) database of known vulnerabilities. Each
13+ vulnerable package that has a fix gets an upgrade issue, and a g1t agent
14+ lands the upgrade through the usual pull request, checks, review and merge
15+ queue.
16+
17+Only members of the workspace can open the Security page, whether the
18+project is public or private. The workspace's own page,
19+`g1t.sh/<owner>/-/security`, lists every project's open findings, most
20+severe first.
21+
22+## The overview
23+
24+The top of the page counts open findings by severity: critical, high,
25+medium, low and unrated. A secret that is open, or that stopped a push,
26+counts as critical. Below the counts:
27+
28+- **Re-scan now** reads the dependencies again at once and scans the
29+ history again from the start.
30+- **Upkeep agents** turns upgrade issues on or off for the project. It is on
31+ unless someone turns it off. With it off, findings are still listed, and
32+ nothing is opened for them.
33+
34+The **Secrets** and **Dependencies** tabs list each finding with its status
35+and the issue or pull request fixing it.
36+
37+## Secret scanning
38+
39+g1t looks for credentials whose format their issuer made recognisable, so a
40+match is nearly always a real secret or a fake made to look like one:
41+
42+| What | What it looks like |
43+| --- | --- |
44+| AWS access keys | `AKIA` or `ASIA` and 16 more characters |
45+| AWS secret access keys | 40 characters on a line that names an AWS secret |
46+| GitHub tokens | `ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_`, `github_pat_` |
47+| GitLab tokens | `glpat-`, `gloas-`, `glrt-`, `glptt-`, `gldt-` |
48+| Stripe live keys | `sk_live_`, `rk_live_` (test keys are left alone) |
49+| Slack | `xoxb-`, `xoxp-` and other tokens, and incoming webhook addresses |
50+| Google API keys | `AIza` and 35 more characters |
51+| Anthropic API keys | `sk-ant-` |
52+| OpenAI API keys | `sk-proj-`, `sk-svcacct-`, `sk-admin-`, and older `sk-` keys |
53+| Private keys | A PEM `BEGIN … PRIVATE KEY` header followed by the key |
54+| Service-role JWTs | A JWT whose claims carry `service_role` |
55+| npm tokens | `npm_` |
56+| g1t tokens | `g1t_` and 40 hex characters |
57+| SendGrid keys | `SG.` and two dotted parts |
58+
59+Placeholders such as `ghp_xxxxxxxx…` are not reported. Lockfiles, and files
60+under `node_modules/` and `vendor/`, are not scanned.
61+
62+g1t never stores a secret it finds. It keeps a fingerprint, so it can
63+recognise the same secret again, and a short preview, such as `AKIA…`, so
64+you can recognise it.
65+
66+### Push protection
67+
68+When a push over HTTPS adds a secret, g1t refuses the whole push and nothing
69+is stored. Only the lines the push adds are checked, so a secret that is
70+already in the repository does not block every later push to the same file.
71+This applies to every push, including the pushes g1t's agents make to their
72+pull requests.
73+
74+Git shows why, file and line:
75+
76+```text
77+$ git push
78+remote: g1t found a secret in this push, so nothing was pushed.
79+remote:
80+remote: config/prod.env:3 an AWS access key (commit 4807077)
81+remote:
82+remote: Take the secret out of the commit that adds it (git commit --amend, or
83+remote: git rebase -i for an older commit), rotate it if it was ever real, and
84+remote: push again.
85+remote:
86+remote: If it is not a real secret, such as a test fixture:
87+remote: - add g1t:allow-secret in a comment on its line, or
88+remote: - allow it once at https://g1t.sh/acme/rocket/security?tab=secrets&finding=sec_…
89+remote: Allowing is recorded with your name, then the same push goes through.
90+To https://g1t.sh/acme/rocket.git
91+ ! [remote rejected] main -> main (secret found: config/prod.env:3 has an AWS access key)
92+```
93+
94+To fix a real secret:
95+
96+1. Remove it from the commit that adds it: `git commit --amend` for the last
97+ commit, `git rebase -i` for an older one.
98+2. Rotate it with whoever issued it. Once a secret has been on any machine
99+ but yours, treat it as known.
100+3. Push again.
101+
102+### Allowing a false positive
103+
104+A fake key in a test, or an example in documentation, is still reported, on
105+purpose: it looks exactly like a real one. Two ways to let it through:
106+
107+- **Mark the line.** Put `g1t:allow-secret` anywhere on the line, usually in
108+ a comment. The line is never reported, in any push or in history.
109+
110+ ```ts
111+ const FIXTURE_KEY = "AKIA…"; // g1t:allow-secret
112+ ```
113+
114+- **Allow it once.** Open the link in git's message (or the finding on the
115+ **Secrets** tab), choose **Allow**, and say why. g1t records who allowed
116+ it and why. Then push again, unchanged: that secret no longer stops a push
117+ to this project.
118+
119+Allowing is for members of the workspace. Someone else pushing to a pull
120+request's fork sees the same message and asks a member.
121+
122+### Secrets in history
123+
124+The first time g1t sees a repository (when it is created, on its next push
125+to the default branch, or when its Security page is first opened) it scans
126+the default branch's history in the background, a page of commits at a
127+time, comparing each commit with its first parent. The page shows how far
128+it has got. Scanning is metered to the workspace like other usage, and it
129+pauses if the workspace reaches its spending limit.
130+
131+A secret found in history is **Open**: it is in the repository, and anyone
132+who could clone it may have it. Rotate it, then choose **Resolve** and say
133+what you did. Removing it from the code is not enough, since it stays in
134+history.
135+
136+| Status | Meaning |
137+| --- | --- |
138+| Open | In the repository's history. Rotate it, then resolve it. |
139+| Push blocked | A push carrying it was refused, so it never landed. |
140+| Allowed | Someone said it is not a real secret; pushes carrying it go through. |
141+| Resolved | Someone rotated or removed it. |
142+
143+**Reopen** undoes an allow or a resolve.
144+
145+## Dependency upkeep
146+
147+g1t reads these lockfiles on the default branch, up to four directories
148+deep, skipping `node_modules`, `vendor`, `target`, `dist` and `build`:
149+
150+| Ecosystem | Files |
151+| --- | --- |
152+| npm | `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock` |
153+| Cargo | `Cargo.lock` |
154+| Go | `go.mod` (or `go.sum` where there is no `go.mod`) |
155+| Python | `poetry.lock`, and pinned lines (`name==1.2.3`) in `requirements.txt` |
156+
157+It reads them on every push to the default branch and again every day, and
158+asks OSV about every package at the exact version locked. Each advisory
159+found is listed with its id (its GHSA id when it has one), severity, the
160+version that fixes it, and the lockfile that resolves the vulnerable
161+version. A vulnerability that a later scan no longer finds is marked fixed.
162+
163+### Upgrade issues
164+
165+With **Upkeep agents** on, each vulnerable package that has a fixed version
166+gets one issue, labelled `dependencies` and `security`, such as:
167+
168+> Upgrade lodash to 4.17.21: fixes GHSA-35jh-r3h4-6jhm
169+
170+The issue lists every advisory it fixes, and the target is the lowest
171+version that fixes all of them. Its acceptance checks are:
172+
173+- a command per lockfile that fails while the lockfile still resolves the
174+ vulnerable version, and
175+- the project's tests, run in the lockfile's directory by its package
176+ manager:
177+
178+ | Lockfile | Tests |
179+ | --- | --- |
180+ | `package-lock.json` | `npm ci && npm test --if-present` |
181+ | `pnpm-lock.yaml` | `pnpm install --frozen-lockfile && pnpm test --if-present` |
182+ | `yarn.lock` | `yarn install`, then `yarn test` |
183+ | `Cargo.lock` | `cargo test --locked` |
184+ | `go.mod`, `go.sum` | `go test ./...` |
185+
186+ Python projects get the lockfile check only, since there is no one way
187+ to run their tests.
188+
189+g1t puts its agent on the first new upgrade at once and queues the rest,
190+which start as the project has room for more agents. The agent upgrades the
191+package, changes whatever code the upgrade breaks, and opens a pull request
192+that goes through checks, review and the merge queue like any other. When it
193+merges, the next scan finds the vulnerability fixed.
194+
195+g1t opens at most eight upgrade issues per scan, most severe first, and
196+never a second issue for a package while one is open. If you close an
197+upgrade issue as not planned, g1t does not open it again. If agents cannot
198+run in the workspace, the issue stays open with a comment saying why, for
199+you to assign once they can, or to upgrade by hand.
200+
201+Turn **Upkeep agents** off on the Security page to stop new upgrade issues
202+for a project. Issues already open are left as they are.
+1−1
5050 -d '{"body": "Keep the old flag working too."}'
5151 ```
5252
53−The response is the message. `deliveredAt` is null until the agent has
53+The response is the message. `delivered_at` is null until the agent has
5454 received it.
5555
5656 ## Ask for changes
+5−5
3636 "type": "comment.created",
3737 "time": "2026-10-03T04:54:37.708Z",
3838 "workspace": "acme",
39− "repository": { "id": "rep_cf985171afeee00a62a1c0acb0", "fullName": "acme/web" },
39+ "repository": { "id": "rep_cf985171afeee00a62a1c0acb0", "full_name": "acme/web" },
4040 "actor": { "id": "usr_b51a1a09fc53e9471cbe1426b7", "username": "ada" },
41− "data": { "commentId": "cmt_01m401te9eekb92kccgwhympr4", "number": 3, "repoId": "rep_cf985171afeee00a62a1c0acb0" }
41+ "data": { "comment_id": "cmt_01m401te9eekb92kccgwhympr4", "number": 3, "repo_id": "rep_cf985171afeee00a62a1c0acb0" }
4242 }
4343 ```
4444
6060
6161 | Event | When |
6262 | --- | --- |
63−| `git.push` | A branch moved. `data.ref`, `data.after`, `data.defaultBranch`. |
63+| `git.push` | A branch moved. `data.ref`, `data.after`, `data.default_branch`. |
6464 | `repo.created`, `repo.forked` | A repository was made, or forked for a pull request. |
65−| `issue.opened`, `issue.updated`, `issue.assigned`, `issue.closed`, `issue.reopened` | An issue changed. `data.number`; on close, `data.reason` and `data.resolvedBy`. |
65+| `issue.opened`, `issue.updated`, `issue.assigned`, `issue.closed`, `issue.reopened` | An issue changed. `data.number`; on close, `data.reason` and `data.resolved_by`. |
6666 | `comment.created` | A comment or review on an issue or pull request. |
6767 | `pull.opened`, `pull.ready`, `pull.updated`, `pull.merge_requested`, `pull.merged`, `pull.closed` | A pull request changed. `data.number`, `data.issue`; on merge, `data.commit`. |
6868 | `checks.completed` | An issue's acceptance checks finished on a pull request. `data.status` is `passed`, `failed` or `errored`. |
6969 | `review.completed` | A g1t agent reviewed a pull request. `data.verdict`. |
70−| `workflow.completed` | A GitHub Actions run finished. `data.workflow`, `data.conclusion`, `data.runId`, `data.sha`, `data.pull`. |
70+| `workflow.completed` | A GitHub Actions run finished. `data.workflow`, `data.conclusion`, `data.run_id`, `data.sha`, `data.pull`. |
7171 | `queue.changed` | The merge queue gained, lost or settled an entry. |
7272 | `session.appended` | An agent's session grew. Busy: choose it only if you need it. |
7373 | `agent.asked` | An agent asked the agent on another pull request a question, or handed it work, while that one was not at work; g1t wakes it to answer. |
+4−0
4949 Every pull request gets a [live preview on g1t.page](/guides/deployments/), and
5050 the default branch goes to production. Apps cost nothing while no one visits.
5151 </Card>
52+ <Card title="Secrets kept out, upgrades landed" icon="warning">
53+ Pushes that add a key or a token are refused, and each vulnerable dependency
54+ gets an [upgrade issue that an agent lands](/guides/security/).
55+ </Card>
5256 <Card title="Your models, your tools" icon="puzzle">
5357 Use g1t's models or [your own providers](/guides/models/), and pull context
5458 from [Sentry, Jira and Linear](/guides/integrations/).
+7−3
7878
7979 Request bodies are JSON.
8080
81−Responses use `camelCase`. Request bodies take the same names as the MCP
82−tools, in `snake_case`, and also accept `camelCase`, so you can send back a
83−field exactly as you read it:
81+Every name in a body is `snake_case`, both ways: responses, errors, MCP
82+results and [webhook](/guides/webhooks/) payloads. Request bodies take the
83+same names as the MCP tools, and also accept the `camelCase` spelling:
8484
8585 ```sh
8686 # Both turn off counting agents' approvals.
9292
9393 When a body gives a field both ways, the `snake_case` one is used.
9494
95+Names you chose are never changed: a workflow's `inputs`, the names of
96+secrets and variables, an environment's `env`, a job's `outputs` and
97+`matrix`, labels and headers come back exactly as they were written.
98+
9599 A successful request answers `200` with the result as the body: an object,
96100 a list, or `true` for a deletion. There is no envelope around it.
97101
+25−8
1313 - `repo` is always `owner/name`, such as `"syntaqx/hello"`.
1414 - `number` names an issue or a pull request. The two share one sequence per
1515 repository, so a number names exactly one of them.
16−- Inputs are `snake_case`. Results are JSON, with `camelCase` fields.
16+- Inputs are `snake_case`. Results are JSON, with `snake_case` fields, as
17+ the REST API returns them.
1718 - A tool that fails returns its error as the result, with `isError` set, so
1819 the agent can read it and act on it.
1920 - Reading a public repository needs no sign-in through the API. Through MCP,
8990 | --- | --- | --- | --- |
9091 | `remember` | `repo`, `text` | Save one fact, convention, decision or gotcha for the next agent. `scope` is `project` (this codebase, the default) or `workspace` (true across its projects); `kind` is `fact`, `convention`, `decision` or `gotcha`. Text that looks like a secret is refused. Members and g1t's agents only. | [`POST /repos/{owner}/{name}/memory`](/reference/api/memory/remember/) |
9192 | `recall` | `repo` | What the project and its workspace remember, pinned first. `query` matches every word; `limit` caps each level. | [`GET /repos/{owner}/{name}/memory`](/reference/api/memory/recall/) |
93+| `search_context` | `query` | One search across a workspace's context hub: its catalog, docs, issues and pull requests, and, for members and g1t's agents, its kept memory. Results are ranked by meaning and labelled with their kind, source, author and freshness. Give `workspace`, or a `repo` in it; narrow with `project` and `kinds`. | [`GET /workspaces/{workspace}/context/search`](/reference/api/context/search-context/) |
94+| `get_entity` | `kind`, `id` | One catalog entry by kind and id or key (a project's slug, a package as `npm:<name>`, an owner's username), with what it depends on, who owns it, where it deploys, what documents it, and what it exposes and uses. | [`GET /workspaces/{workspace}/context/{kind}/{id}`](/reference/api/context/get-entity/) |
9295
9396 See [agents, sessions and memory](/guides/agents-and-memory/).
9497
174177
175178 ## What a g1t agent can use
176179
177−A g1t agent works with a token limited to its own repository and to these
178−tools: `get_repo`, `list_issues`, `get_issue`, `list_labels`,
179−`create_issue`, `add_comment`, `list_pull_requests`, `get_pull_request`,
180−`get_pull_request_changes`, `read_session`, `get_merge_queue`,
181−`list_events`, `take_messages`, `message_agent`, `answer_message`, `remember`,
182−`recall` and `get_context`.
183−`tools/list` shows such a token only the tools it may use.
180+A g1t agent works with a [run credential](/guides/g1t-agents/#credentials):
181+a token bound to its run and its own repository, acting as `g1t-agent` on
182+behalf of the person who started the work, and only while that person is
183+still a member of the workspace. Which tools it may use depends on the kind
184+of run.
185+
186+| Run | Tools |
187+| --- | --- |
188+| Implement, revise, answer | `get_repo`, `list_issues`, `get_issue`, `list_labels`, `list_pull_requests`, `get_pull_request`, `get_pull_request_changes`, `read_session`, `get_merge_queue`, `list_events`, `recall`, `search_context`, `get_entity`, `list_workflows`, `list_workflow_runs`, `get_workflow_run`, `get_job_logs`, and `create_issue`, `add_comment`, `take_messages`, `remember`, `message_agent`, `answer_message`, `get_context` |
189+| Review | The same reading tools, and `add_comment`, `review_pull_request`, `get_context` |
190+| Plan | The same reading tools, and `create_issue`, `get_context` |
191+| Catch up | The reading tools only |
192+
193+No agent's token can use the tools for settings, members, tokens, billing,
194+integrations, webhooks, secrets and variables, or workflows' controls, nor
195+`merge_pull_request`, `assign_issue`, `plan_work`, `apply_plan`,
196+`import_issue`, `create_repo` or `create_workspace`. Every repository it
197+names must be its own. `tools/list` shows such a token only the tools it
198+may use; a call to any other is refused with the rule that refused it, and
199+recorded in the workspace's [audit log](/guides/audit-log/), as is every
200+call it makes.
+3−0
1+{
2+ "page.editLink": "View this page on g1t"
3+}
+832−466
33 "info": {
44 "title": "g1t API",
55 "version": "1",
6− "description": "The REST API for g1t, a git forge built for agents. The same operations are available to agents as MCP tools at https://mcp.g1t.sh.",
6+ "description": "The REST API for g1t, a git forge built for agents. The same operations are available to agents as MCP tools at https://mcp.g1t.sh. Every name in a request or response body is `snake_case`; names you chose, such as a workflow's inputs or a secret's name, are returned as you wrote them.",
77 "license": {
88 "name": "MIT",
99 "identifier": "MIT"
107107 ]
108108 },
109109 {
110+ "name": "Context",
111+ "description": "A workspace's context hub: a catalog of what it builds and runs, built from its repositories, deployments and integrations, and one search across the catalog, docs, issues, pull requests and memory.",
112+ "x-tools": [
113+ "search_context",
114+ "get_entity"
115+ ]
116+ },
117+ {
110118 "name": "Actions",
111119 "description": "GitHub Actions workflows in .g1t/workflows, their runs, and their jobs' logs.",
112120 "x-tools": [
399407 "slug": "syntaqx-labs",
400408 "name": "Syntaqx Labs",
401409 "description": null,
402− "createdAt": "2026-10-04T16:02:51.337Z",
403− "memberCount": 1
410+ "created_at": "2026-10-04T16:02:51.337Z",
411+ "member_count": 1
404412 }
405413 }
406414 }
523531 "namespace": "syntaqx",
524532 "name": "hello",
525533 "description": "A tiny service that says hello.",
526− "isPrivate": false,
527− "ownerId": "usr_01kkntcg1eeb98j62xjm7eh09p",
528− "defaultBranch": "main",
529− "forkOf": null,
534+ "is_private": false,
535+ "owner_id": "usr_01kkntcg1eeb98j62xjm7eh09p",
536+ "default_branch": "main",
537+ "fork_of": null,
530538 "protected": true,
531− "createdAt": "2026-09-28T14:11:52.640Z"
539+ "created_at": "2026-09-28T14:11:52.640Z"
532540 }
533541 ]
534542 }
575583 "Repositories"
576584 ],
577585 "summary": "Create a repository",
578− "description": "Create a repository in one of your workspaces, empty or as a copy of a public git repository elsewhere.\n\n`ownerId` is the id of the person who created the repository. With `import_url`, `defaultBranch` is the default branch of the repository copied.",
586+ "description": "Create a repository in one of your workspaces, empty or as a copy of a public git repository elsewhere.\n\n`owner_id` is the id of the person who created the repository. With `import_url`, `default_branch` is the default branch of the repository copied.",
579587 "x-mcp-tool": "create_repo",
580588 "security": [
581589 {
594602 "namespace": "syntaqx",
595603 "name": "hello-cli",
596604 "description": "Command-line client for hello.",
597− "isPrivate": false,
598− "ownerId": "usr_01kkntcg1eeb98j62xjm7eh09p",
599− "defaultBranch": "main",
600− "forkOf": null,
605+ "is_private": false,
606+ "owner_id": "usr_01kkntcg1eeb98j62xjm7eh09p",
607+ "default_branch": "main",
608+ "fork_of": null,
601609 "protected": false,
602− "createdAt": "2026-10-04T16:05:12.913Z"
610+ "created_at": "2026-10-04T16:05:12.913Z"
603611 }
604612 }
605613 }
741749 "namespace": "syntaqx",
742750 "name": "hello",
743751 "description": "A tiny service that says hello.",
744− "isPrivate": false,
745− "ownerId": "usr_01kkntcg1eeb98j62xjm7eh09p",
746− "defaultBranch": "main",
747− "forkOf": null,
752+ "is_private": false,
753+ "owner_id": "usr_01kkntcg1eeb98j62xjm7eh09p",
754+ "default_branch": "main",
755+ "fork_of": null,
748756 "protected": true,
749− "createdAt": "2026-09-28T14:11:52.640Z"
757+ "created_at": "2026-09-28T14:11:52.640Z"
750758 }
751759 }
752760 }
837845 "namespace": "syntaqx",
838846 "name": "hello",
839847 "description": "Says hello, politely.",
840− "isPrivate": false,
841− "ownerId": "usr_01kkntcg1eeb98j62xjm7eh09p",
842− "defaultBranch": "main",
843− "forkOf": null,
848+ "is_private": false,
849+ "owner_id": "usr_01kkntcg1eeb98j62xjm7eh09p",
850+ "default_branch": "main",
851+ "fork_of": null,
844852 "protected": true,
845− "createdAt": "2026-09-28T14:11:52.640Z"
853+ "created_at": "2026-09-28T14:11:52.640Z"
846854 }
847855 }
848856 }
969977 "application/json": {
970978 "schema": {},
971979 "example": {
972− "autoMerge": false,
973− "requireUpToDate": false,
974− "requiredApprovals": 0,
975− "countAgentApprovals": true,
976− "allowIgnoringChecks": true,
977− "agentReview": true,
978− "maxRevisions": 2,
979− "mergeQueue": false,
980− "updatedBy": null,
981− "updatedAt": null
980+ "auto_merge": false,
981+ "require_up_to_date": false,
982+ "required_approvals": 0,
983+ "count_agent_approvals": true,
984+ "allow_ignoring_checks": true,
985+ "agent_review": true,
986+ "max_revisions": 2,
987+ "merge_queue": false,
988+ "updated_by": null,
989+ "updated_at": null
982990 }
983991 }
984992 }
10651073 "application/json": {
10661074 "schema": {},
10671075 "example": {
1068− "autoMerge": false,
1069− "requireUpToDate": false,
1070− "requiredApprovals": 1,
1071− "countAgentApprovals": false,
1072− "allowIgnoringChecks": true,
1073− "agentReview": true,
1074− "maxRevisions": 2,
1075− "mergeQueue": true,
1076− "updatedBy": "syntaqx",
1077− "updatedAt": "2026-10-04T16:20:37.508Z"
1076+ "auto_merge": false,
1077+ "require_up_to_date": false,
1078+ "required_approvals": 1,
1079+ "count_agent_approvals": false,
1080+ "allow_ignoring_checks": true,
1081+ "agent_review": true,
1082+ "max_revisions": 2,
1083+ "merge_queue": true,
1084+ "updated_by": "syntaqx",
1085+ "updated_at": "2026-10-04T16:20:37.508Z"
10781086 }
10791087 }
10801088 }
12321240 "agent": "claude-code",
12331241 "state": "testing",
12341242 "ahead": [],
1235− "baseCommit": "3a7e9c1b5d2f4a6c8e0b2d4f6a8c0e2b4d6f8a1c",
1236− "combinedCommit": null,
1243+ "base_commit": "3a7e9c1b5d2f4a6c8e0b2d4f6a8c0e2b4d6f8a1c",
1244+ "combined_commit": null,
12371245 "error": null,
12381246 "results": [],
1239− "enqueuedBy": "syntaqx",
1240− "createdAt": "2026-10-04T15:31:20.441Z",
1241− "finishedAt": null
1247+ "enqueued_by": "syntaqx",
1248+ "created_at": "2026-10-04T15:31:20.441Z",
1249+ "finished_at": null
12421250 }
12431251 ],
12441252 "recent": [
12491257 "agent": "g1t-agent",
12501258 "state": "landed",
12511259 "ahead": [],
1252− "baseCommit": "4b1d7e9c2a5f8e3d6c0b9a7e5d3c1b8a6f4e2d0c",
1253− "combinedCommit": "c3e8a1f6d4b2097e5c3a1f8d6b4e2c0a9f7d5b3e",
1260+ "base_commit": "4b1d7e9c2a5f8e3d6c0b9a7e5d3c1b8a6f4e2d0c",
1261+ "combined_commit": "c3e8a1f6d4b2097e5c3a1f8d6b4e2c0a9f7d5b3e",
12541262 "error": null,
12551263 "results": [
12561264 {
12571265 "command": "cargo test",
12581266 "passed": true,
1259− "exitCode": 0,
1267+ "exit_code": 0,
12601268 "output": "test result: ok. 12 passed; 0 failed\n",
1261− "durationMs": 48211
1269+ "duration_ms": 48211
12621270 }
12631271 ],
1264− "enqueuedBy": "g1t",
1265− "createdAt": "2026-10-04T14:02:09.876Z",
1266− "finishedAt": "2026-10-04T14:09:55.102Z"
1272+ "enqueued_by": "g1t",
1273+ "created_at": "2026-10-04T14:02:09.876Z",
1274+ "finished_at": "2026-10-04T14:09:55.102Z"
12671275 }
12681276 ]
12691277 }
13201328 "Pull requests"
13211329 ],
13221330 "summary": "Message an agent",
1323− "description": "Send the agent working on a pull request a message: a correction, a hint, a change of plan. It receives it at its next step, and it is recorded in the pull request's session. The pull request's author and members of its workspace only. An agent uses it to ask the agent on another pull request a question (kind: question) or hand it work that belongs there (kind: handoff), giving its own pull request as from_number; the answer comes back to it at its next step.\n\nThe response is the message. `deliveredAt` is set once the agent has read it.",
1331+ "description": "Send the agent working on a pull request a message: a correction, a hint, a change of plan. It receives it at its next step, and it is recorded in the pull request's session. The pull request's author and members of its workspace only. An agent uses it to ask the agent on another pull request a question (kind: question) or hand it work that belongs there (kind: handoff), giving its own pull request as from_number; the answer comes back to it at its next step.\n\nThe response is the message. `delivered_at` is set once the agent has read it.",
13241332 "x-mcp-tool": "message_agent",
13251333 "security": [
13261334 {
13661374 "id": "msg_01m43tx5egeh4t3f9zcnjenqvz",
13671375 "author": "syntaqx",
13681376 "body": "Keep \"world\" as the default when no name is given.",
1369− "createdAt": "2026-10-01T18:25:00.310Z",
1370− "deliveredAt": null,
1377+ "created_at": "2026-10-01T18:25:00.310Z",
1378+ "delivered_at": null,
13711379 "kind": "message",
1372− "fromNumber": null,
1373− "toNumber": 14,
1380+ "from_number": null,
1381+ "to_number": 14,
13741382 "answer": null,
13751383 "declined": false
13761384 }
15181526 "id": "msg_01m43tx5egeh4t3f9zcnjenqvz",
15191527 "author": "syntaqx",
15201528 "body": "Keep \"world\" as the default when no name is given.",
1521− "createdAt": "2026-10-01T18:25:00.310Z",
1522− "deliveredAt": "2026-10-01T18:25:31.007Z",
1529+ "created_at": "2026-10-01T18:25:00.310Z",
1530+ "delivered_at": "2026-10-01T18:25:31.007Z",
15231531 "kind": "message",
1524− "fromNumber": null,
1525− "toNumber": 14,
1532+ "from_number": null,
1533+ "to_number": 14,
15261534 "answer": null,
15271535 "declined": false
15281536 }
16361644 "id": "msg_01m43t66tde8hs2vpxhh3v8tqw",
16371645 "author": "g1t-agent",
16381646 "body": "Are you renaming greet() in src/lib.rs? I need to call it from #16.",
1639− "createdAt": "2026-10-01T18:58:12.301Z",
1640− "deliveredAt": "2026-10-01T18:58:40.117Z",
1647+ "created_at": "2026-10-01T18:58:12.301Z",
1648+ "delivered_at": "2026-10-01T18:58:40.117Z",
16411649 "kind": "question",
1642− "fromNumber": 16,
1643− "toNumber": 14,
1650+ "from_number": 16,
1651+ "to_number": 14,
16441652 "answer": "No. greet() keeps its name; only greet_named() is added.",
16451653 "declined": false
16461654 }
17811789 "kind": "gotcha",
17821790 "source": {
17831791 "kind": "run",
1784− "runId": "arn_01m43v2c1p9k8d7e6f5a4b3c2d",
1792+ "run_id": "arn_01m43v2c1p9k8d7e6f5a4b3c2d",
17851793 "repo": {
17861794 "namespace": "syntaqx",
17871795 "name": "hello"
17881796 },
17891797 "number": 14
17901798 },
1791− "createdBy": "g1t-agent",
1799+ "created_by": "g1t-agent",
17921800 "pinned": false,
1793− "createdAt": "2026-10-01T18:40:12.000Z",
1794− "updatedAt": "2026-10-01T18:40:12.000Z",
1795− "lastUsedAt": null
1801+ "created_at": "2026-10-01T18:40:12.000Z",
1802+ "updated_at": "2026-10-01T18:40:12.000Z",
1803+ "last_used_at": null
17961804 }
17971805 }
17981806 }
19651973 "kind": "convention",
19661974 "source": {
19671975 "kind": "person",
1968− "runId": null,
1976+ "run_id": null,
19691977 "repo": null,
19701978 "number": null
19711979 },
1972− "createdBy": "syntaqx",
1980+ "created_by": "syntaqx",
19731981 "pinned": true,
1974− "createdAt": "2026-10-01T18:40:12.000Z",
1975− "updatedAt": "2026-10-01T18:40:12.000Z",
1976− "lastUsedAt": null
1982+ "created_at": "2026-10-01T18:40:12.000Z",
1983+ "updated_at": "2026-10-01T18:40:12.000Z",
1984+ "last_used_at": null
19771985 }
19781986 ]
19791987 }
20822090 "type": "pull.opened",
20832091 "source": "work",
20842092 "time": "2026-10-01T18:20:02.117Z",
2085− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
2093+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
20862094 "actor": "usr_01kkntcg1eeb98j62xjm7eh09p",
20872095 "data": {
2088− "pullId": "pr_01m43smh3vexsr5pmp60qwv0vs",
2089− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
2096+ "pull_id": "pr_01m43smh3vexsr5pmp60qwv0vs",
2097+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
20902098 "number": 14,
20912099 "issue": 12,
20922100 "agent": "claude-code"
20972105 "type": "issue.opened",
20982106 "source": "work",
20992107 "time": "2026-10-01T18:04:11.482Z",
2100− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
2108+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
21012109 "actor": "usr_01kkntcg1eeb98j62xjm7eh09p",
21022110 "data": {
2103− "issueId": "iss_01m43shrzpfe49x74ga7sj1c6v",
2104− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
2111+ "issue_id": "iss_01m43shrzpfe49x74ga7sj1c6v",
2112+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
21052113 "number": 12,
21062114 "title": "Greeting should name the caller"
21072115 }
22582266 "Issues"
22592267 ],
22602268 "summary": "List issues",
2261− "description": "Issues on a repository, newest first. An issue is something that should change: a bug, a feature, a question. Pull requests are made against it.\n\nReturns at most 100 issues, newest first. `commentCount` counts comments, not events such as \"opened #14 for this\".",
2269+ "description": "Issues on a repository, newest first. An issue is something that should change: a bug, a feature, a question. Pull requests are made against it.\n\nReturns at most 100 issues, newest first. `comment_count` counts comments, not events such as \"opened #14 for this\".",
22622270 "x-mcp-tool": "list_issues",
22632271 "security": [
22642272 {
23162324 "example": [
23172325 {
23182326 "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
2319− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
2327+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
23202328 "number": 12,
23212329 "title": "Greeting should name the caller",
23222330 "body": "Take a name from the first argument; fall back to world.",
23282336 ],
23292337 "state": "open",
23302338 "reason": null,
2331− "resolvedBy": null,
2339+ "resolved_by": null,
23322340 "author": {
23332341 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
23342342 "username": "syntaqx",
23362344 "verified": false,
23372345 "workspaces": []
23382346 },
2339− "createdAt": "2026-10-01T18:04:11.482Z",
2340− "updatedAt": "2026-10-01T18:04:11.482Z",
2341− "closedAt": null,
2342− "pullCount": 1,
2343− "commentCount": 0,
2347+ "created_at": "2026-10-01T18:04:11.482Z",
2348+ "updated_at": "2026-10-01T18:04:11.482Z",
2349+ "closed_at": null,
2350+ "pull_count": 1,
2351+ "comment_count": 0,
23442352 "assignees": [],
2345− "blockedBy": [],
2353+ "blocked_by": [],
23462354 "queued": false,
23472355 "agent": "claude-code"
23482356 }
24372445 "schema": {},
24382446 "example": {
24392447 "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
2440− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
2448+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
24412449 "number": 12,
24422450 "title": "Greeting should name the caller",
24432451 "body": "Take a name from the first argument; fall back to world.",
24492457 ],
24502458 "state": "open",
24512459 "reason": null,
2452− "resolvedBy": null,
2460+ "resolved_by": null,
24532461 "author": {
24542462 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
24552463 "username": "syntaqx",
24572465 "verified": false,
24582466 "workspaces": []
24592467 },
2460− "createdAt": "2026-10-01T18:04:11.482Z",
2461− "updatedAt": "2026-10-01T18:04:11.482Z",
2462− "closedAt": null,
2463− "pullCount": 0,
2464− "commentCount": 0,
2468+ "created_at": "2026-10-01T18:04:11.482Z",
2469+ "updated_at": "2026-10-01T18:04:11.482Z",
2470+ "closed_at": null,
2471+ "pull_count": 0,
2472+ "comment_count": 0,
24652473 "assignees": [],
2466− "blockedBy": [],
2474+ "blocked_by": [],
24672475 "queued": false,
24682476 "agent": null
24692477 }
25772585 "Issues"
25782586 ],
25792587 "summary": "Get an issue",
2580− "description": "An issue: its description, labels and acceptance checks, its comments, and every pull request made against it with its status. If the issue is closed, resolvedBy is the number of the pull request that was merged for it. Read this before opening a pull request, to see what others have already tried.\n\nA closed issue says how it was closed: `reason` is `completed` or `not_planned`, and `resolvedBy` is the number of the pull request whose merge closed it. Each pull request made for it is listed; one closed because another was merged has `supersededBy` set.",
2588+ "description": "An issue: its description, labels and acceptance checks, its comments, and every pull request made against it with its status. If the issue is closed, resolvedBy is the number of the pull request that was merged for it. Read this before opening a pull request, to see what others have already tried.\n\nA closed issue says how it was closed: `reason` is `completed` or `not_planned`, and `resolved_by` is the number of the pull request whose merge closed it. Each pull request made for it is listed; one closed because another was merged has `superseded_by` set.",
25812589 "x-mcp-tool": "get_issue",
25822590 "security": [
25832591 {
26232631 "example": {
26242632 "issue": {
26252633 "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
2626− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
2634+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
26272635 "number": 12,
26282636 "title": "Greeting should name the caller",
26292637 "body": "Take a name from the first argument; fall back to world.",
26352643 ],
26362644 "state": "closed",
26372645 "reason": "completed",
2638− "resolvedBy": 14,
2646+ "resolved_by": 14,
26392647 "author": {
26402648 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
26412649 "username": "syntaqx",
26432651 "verified": false,
26442652 "workspaces": []
26452653 },
2646− "createdAt": "2026-10-01T18:04:11.482Z",
2647− "updatedAt": "2026-10-01T18:52:17.093Z",
2648− "closedAt": "2026-10-01T18:52:17.093Z",
2649− "pullCount": 2,
2650− "commentCount": 0,
2654+ "created_at": "2026-10-01T18:04:11.482Z",
2655+ "updated_at": "2026-10-01T18:52:17.093Z",
2656+ "closed_at": "2026-10-01T18:52:17.093Z",
2657+ "pull_count": 2,
2658+ "comment_count": 0,
26512659 "assignees": [],
2652− "blockedBy": [],
2660+ "blocked_by": [],
26532661 "queued": false,
26542662 "agent": null
26552663 },
26562664 "pulls": [
26572665 {
26582666 "id": "pr_01m43sjq8tcz5rbm2a7k4d9e6w",
2659− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
2667+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
26602668 "number": 13,
26612669 "issue": 12,
26622670 "title": "Greeting should name the caller",
26682676 "namespace": "pulls",
26692677 "name": "pr_01m43sjq8tcz5rbm2a7k4d9e6w"
26702678 },
2671− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
2679+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
26722680 "branch": null,
2673− "headCommit": null,
2674− "mergeBase": null,
2675− "mergedBy": null,
2676− "mergedAt": null,
2677− "supersededBy": 14,
2678− "checkStatus": null,
2681+ "head_commit": null,
2682+ "merge_base": null,
2683+ "merged_by": null,
2684+ "merged_at": null,
2685+ "superseded_by": 14,
2686+ "check_status": null,
26792687 "files": [],
26802688 "assignees": [],
26812689 "reviewers": [],
26862694 "verified": false,
26872695 "workspaces": []
26882696 },
2689− "createdAt": "2026-10-01T18:20:02.117Z",
2690− "updatedAt": "2026-10-01T18:20:02.117Z"
2697+ "created_at": "2026-10-01T18:20:02.117Z",
2698+ "updated_at": "2026-10-01T18:20:02.117Z"
26912699 },
26922700 {
26932701 "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
2694− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
2702+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
26952703 "number": 14,
26962704 "issue": 12,
26972705 "title": "Greeting should name the caller",
27032711 "namespace": "pulls",
27042712 "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
27052713 },
2706− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
2714+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
27072715 "branch": null,
2708− "headCommit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
2709− "mergeBase": "3a7e9c1b5d2f4a6c8e0b2d4f6a8c0e2b4d6f8a1c",
2710− "mergedBy": "syntaqx",
2711− "mergedAt": "2026-10-01T18:52:17.093Z",
2712− "supersededBy": null,
2713− "checkStatus": "passed",
2716+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
2717+ "merge_base": "3a7e9c1b5d2f4a6c8e0b2d4f6a8c0e2b4d6f8a1c",
2718+ "merged_by": "syntaqx",
2719+ "merged_at": "2026-10-01T18:52:17.093Z",
2720+ "superseded_by": null,
2721+ "check_status": "passed",
27142722 "files": [
27152723 {
27162724 "path": "src/main.rs",
27292737 "verified": false,
27302738 "workspaces": []
27312739 },
2732− "createdAt": "2026-10-01T18:20:02.117Z",
2733− "updatedAt": "2026-10-01T18:52:17.093Z"
2740+ "created_at": "2026-10-01T18:20:02.117Z",
2741+ "updated_at": "2026-10-01T18:52:17.093Z"
27342742 }
27352743 ],
27362744 "comments": [
27482756 "path": null,
27492757 "line": null,
27502758 "verdict": null,
2751− "createdAt": "2026-10-01T18:20:02.141Z"
2759+ "created_at": "2026-10-01T18:20:02.141Z"
27522760 }
27532761 ]
27542762 }
28472855 "schema": {},
28482856 "example": {
28492857 "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
2850− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
2858+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
28512859 "number": 12,
28522860 "title": "Greeting should name the caller",
28532861 "body": "Take a name from the first argument; fall back to world.",
28602868 ],
28612869 "state": "open",
28622870 "reason": null,
2863− "resolvedBy": null,
2871+ "resolved_by": null,
28642872 "author": {
28652873 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
28662874 "username": "syntaqx",
28682876 "verified": false,
28692877 "workspaces": []
28702878 },
2871− "createdAt": "2026-10-01T18:04:11.482Z",
2872− "updatedAt": "2026-10-01T18:09:47.305Z",
2873− "closedAt": null,
2874− "pullCount": 0,
2875− "commentCount": 0,
2879+ "created_at": "2026-10-01T18:04:11.482Z",
2880+ "updated_at": "2026-10-01T18:09:47.305Z",
2881+ "closed_at": null,
2882+ "pull_count": 0,
2883+ "comment_count": 0,
28762884 "assignees": [
28772885 "syntaqx"
28782886 ],
2879− "blockedBy": [],
2887+ "blocked_by": [],
28802888 "queued": false,
28812889 "agent": null
28822890 }
30273035 "schema": {},
30283036 "example": {
30293037 "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
3030− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
3038+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
30313039 "number": 12,
30323040 "title": "Greeting should name the caller",
30333041 "body": "Take a name from the first argument; fall back to world.",
30393047 ],
30403048 "state": "closed",
30413049 "reason": "not_planned",
3042− "resolvedBy": null,
3050+ "resolved_by": null,
30433051 "author": {
30443052 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
30453053 "username": "syntaqx",
30473055 "verified": false,
30483056 "workspaces": []
30493057 },
3050− "createdAt": "2026-10-01T18:04:11.482Z",
3051− "updatedAt": "2026-10-01T19:30:00.214Z",
3052− "closedAt": "2026-10-01T19:30:00.214Z",
3053− "pullCount": 0,
3054− "commentCount": 0,
3058+ "created_at": "2026-10-01T18:04:11.482Z",
3059+ "updated_at": "2026-10-01T19:30:00.214Z",
3060+ "closed_at": "2026-10-01T19:30:00.214Z",
3061+ "pull_count": 0,
3062+ "comment_count": 0,
30553063 "assignees": [],
3056− "blockedBy": [],
3064+ "blocked_by": [],
30573065 "queued": false,
30583066 "agent": null
30593067 }
31873195 "schema": {},
31883196 "example": {
31893197 "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
3190− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
3198+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
31913199 "number": 12,
31923200 "title": "Greeting should name the caller",
31933201 "body": "Take a name from the first argument; fall back to world.",
31993207 ],
32003208 "state": "open",
32013209 "reason": null,
3202− "resolvedBy": null,
3210+ "resolved_by": null,
32033211 "author": {
32043212 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
32053213 "username": "syntaqx",
32073215 "verified": false,
32083216 "workspaces": []
32093217 },
3210− "createdAt": "2026-10-01T18:04:11.482Z",
3211− "updatedAt": "2026-10-01T19:41:52.830Z",
3212− "closedAt": null,
3213− "pullCount": 0,
3214− "commentCount": 0,
3218+ "created_at": "2026-10-01T18:04:11.482Z",
3219+ "updated_at": "2026-10-01T19:41:52.830Z",
3220+ "closed_at": null,
3221+ "pull_count": 0,
3222+ "comment_count": 0,
32153223 "assignees": [],
3216− "blockedBy": [],
3224+ "blocked_by": [],
32173225 "queued": false,
32183226 "agent": null
32193227 }
33243332 "schema": {},
33253333 "example": {
33263334 "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
3327− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
3335+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
33283336 "number": 14,
33293337 "issue": 12,
33303338 "title": "Greeting should name the caller",
33363344 "namespace": "pulls",
33373345 "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
33383346 },
3339− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
3347+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
33403348 "branch": null,
3341− "headCommit": null,
3342− "mergeBase": null,
3343− "mergedBy": null,
3344− "mergedAt": null,
3345− "supersededBy": null,
3346− "checkStatus": null,
3349+ "head_commit": null,
3350+ "merge_base": null,
3351+ "merged_by": null,
3352+ "merged_at": null,
3353+ "superseded_by": null,
3354+ "check_status": null,
33473355 "files": [],
33483356 "assignees": [],
33493357 "reviewers": [],
33543362 "verified": false,
33553363 "workspaces": []
33563364 },
3357− "createdAt": "2026-10-01T18:20:02.117Z",
3358− "updatedAt": "2026-10-01T18:20:02.117Z"
3365+ "created_at": "2026-10-01T18:20:02.117Z",
3366+ "updated_at": "2026-10-01T18:20:02.117Z"
33593367 }
33603368 }
33613369 }
34913499 "url": "https://acme.atlassian.net/browse/TECH-1234",
34923500 "status": "In Progress",
34933501 "body": "Customers with 3DS cards see a 500 at /pay.",
3494− "fetchedAt": "2026-10-04T15:42:07.318Z"
3502+ "fetched_at": "2026-10-04T15:42:07.318Z"
34953503 },
34963504 "created": true
34973505 }
36333641 "url": "https://acme.atlassian.net/browse/TECH-1234",
36343642 "status": "In Progress",
36353643 "body": "Customers with 3DS cards see a 500 at /pay.",
3636− "fetchedAt": "2026-10-04T15:42:07.318Z"
3644+ "fetched_at": "2026-10-04T15:42:07.318Z"
36373645 }
36383646 }
36393647 }
36843692 }
36853693 }
36863694 },
3695+ "/workspaces/{workspace}/context/search": {
3696+ "get": {
3697+ "operationId": "search_context",
3698+ "tags": [
3699+ "Context"
3700+ ],
3701+ "summary": "Search the context hub",
3702+ "description": "One search across a workspace's context hub: its catalog (projects, apps, APIs, packages, languages, owners, environments, integrations, docs), the text of its docs, its issues and pull requests, and, for members and g1t's agents, its kept memory. Results are ranked by meaning, each labelled with its kind, where it came from, who wrote it and how fresh it is; matching words answers when meaning cannot. Give the workspace, or a repository in it. Narrow with project (a project's slug) and kinds. Reads only what you may see: memory and private projects are for members.\n\nGive `workspace`, or `repo` as `owner/name` for its workspace. `kinds` narrows to some of `project`, `app`, `api`, `package`, `language`, `owner`, `environment`, `integration`, `doc`, `memory`, `issue` and `pull`; in a URL, comma-separated. `mode` is `text` when the search index could not answer and words were matched instead. Memory, and anything from a private project, is returned only to members of the workspace and its agents. A g1t agent searches its own workspace only.",
3703+ "x-mcp-tool": "search_context",
3704+ "security": [
3705+ {
3706+ "token": []
3707+ }
3708+ ],
3709+ "parameters": [
3710+ {
3711+ "name": "workspace",
3712+ "in": "path",
3713+ "required": true,
3714+ "schema": {
3715+ "type": "string"
3716+ },
3717+ "description": "The workspace's slug, e.g. \"syntaqx\"."
3718+ },
3719+ {
3720+ "name": "q",
3721+ "in": "query",
3722+ "required": true,
3723+ "schema": {
3724+ "type": "string"
3725+ },
3726+ "description": "What you want to know, in words: \"how do we deploy the api\", \"who owns billing\"."
3727+ },
3728+ {
3729+ "name": "project",
3730+ "in": "query",
3731+ "required": false,
3732+ "schema": {
3733+ "type": "string"
3734+ },
3735+ "description": "Only what is about this project, by its slug."
3736+ },
3737+ {
3738+ "name": "kinds",
3739+ "in": "query",
3740+ "required": false,
3741+ "schema": {
3742+ "type": "array",
3743+ "items": {
3744+ "type": "string",
3745+ "enum": [
3746+ "project",
3747+ "app",
3748+ "api",
3749+ "package",
3750+ "language",
3751+ "owner",
3752+ "environment",
3753+ "integration",
3754+ "doc",
3755+ "memory",
3756+ "issue",
3757+ "pull"
3758+ ]
3759+ }
3760+ },
3761+ "description": "Only these kinds. All of them if not given."
3762+ },
3763+ {
3764+ "name": "limit",
3765+ "in": "query",
3766+ "required": false,
3767+ "schema": {
3768+ "type": "integer"
3769+ },
3770+ "description": "At most 50; 20 if not given."
3771+ }
3772+ ],
3773+ "responses": {
3774+ "200": {
3775+ "description": "Success.",
3776+ "content": {
3777+ "application/json": {
3778+ "schema": {},
3779+ "example": {
3780+ "query": "how do we run the web tests",
3781+ "mode": "semantic",
3782+ "hits": [
3783+ {
3784+ "kind": "memory",
3785+ "id": "mem_01m4a0c2b7k3f9d1e5g8h2j6k4",
3786+ "title": "Gotcha",
3787+ "snippet": "The date tests fail unless TZ=UTC.",
3788+ "project": "web",
3789+ "url": "/acme/web/memory",
3790+ "score": 0.82,
3791+ "source": "AGENTS.md",
3792+ "by": "g1t",
3793+ "updated_at": "2026-10-04T21:10:02.000Z"
3794+ },
3795+ {
3796+ "kind": "doc",
3797+ "id": "ent_5f0c2a9e41d7b3c86a1e2f40:2",
3798+ "title": "Web (README.md)",
3799+ "snippet": "## Testing Run npm test. The end-to-end tests need the api running locally…",
3800+ "project": "web",
3801+ "url": "/acme/web/blob/main/README.md",
3802+ "score": 0.77,
3803+ "source": "README.md",
3804+ "by": null,
3805+ "updated_at": "2026-10-04T20:58:41.000Z"
3806+ },
3807+ {
3808+ "kind": "project",
3809+ "id": "ent_9b1d6f0a2c3e4b5d6e7f8a9b",
3810+ "title": "web",
3811+ "snippet": "The storefront. Written in TypeScript. Packages: @acme/web. Uses api. Owned by ana.",
3812+ "project": "web",
3813+ "url": "/acme/web",
3814+ "score": 0.71,
3815+ "source": "catalog",
3816+ "by": null,
3817+ "updated_at": "2026-10-04T20:58:41.000Z"
3818+ }
3819+ ]
3820+ }
3821+ }
3822+ }
3823+ },
3824+ "401": {
3825+ "description": "A token is required, or the one sent is not valid.",
3826+ "content": {
3827+ "application/json": {
3828+ "schema": {
3829+ "$ref": "#/components/schemas/Error"
3830+ }
3831+ }
3832+ }
3833+ },
3834+ "403": {
3835+ "description": "Signed in, but not allowed to do this.",
3836+ "content": {
3837+ "application/json": {
3838+ "schema": {
3839+ "$ref": "#/components/schemas/Error"
3840+ }
3841+ }
3842+ }
3843+ },
3844+ "404": {
3845+ "description": "It does not exist, or you cannot see it.",
3846+ "content": {
3847+ "application/json": {
3848+ "schema": {
3849+ "$ref": "#/components/schemas/Error"
3850+ }
3851+ }
3852+ }
3853+ },
3854+ "422": {
3855+ "description": "The input is not valid.",
3856+ "content": {
3857+ "application/json": {
3858+ "schema": {
3859+ "$ref": "#/components/schemas/Error"
3860+ }
3861+ }
3862+ }
3863+ }
3864+ },
3865+ "x-example-params": {
3866+ "workspace": "acme"
3867+ },
3868+ "x-example-query": {
3869+ "q": "how do we run the web tests",
3870+ "project": "web"
3871+ }
3872+ }
3873+ },
3874+ "/workspaces/{workspace}/context/{kind}/{id}": {
3875+ "get": {
3876+ "operationId": "get_entity",
3877+ "tags": [
3878+ "Context"
3879+ ],
3880+ "summary": "Get a catalog entry",
3881+ "description": "One entry of a workspace's catalog, by kind and its id or key (a project's slug, a package as npm:<name>, an owner's username), with every relation it has: what it depends on, who owns it, where it deploys, what documents it, what it exposes and uses. search_context finds entries.\n\n`id` is the entry's id, or its key: a project's or app's slug, `npm:<name>` (or `cargo:`, `go:`, `pypi:`) for a package, a username for an owner, `<project>/production` for an environment. `data` depends on the kind: a package's version and dependencies, an API's routes, an app's production address.",
3882+ "x-mcp-tool": "get_entity",
3883+ "security": [
3884+ {
3885+ "token": []
3886+ }
3887+ ],
3888+ "parameters": [
3889+ {
3890+ "name": "workspace",
3891+ "in": "path",
3892+ "required": true,
3893+ "schema": {
3894+ "type": "string"
3895+ },
3896+ "description": "The workspace's slug, e.g. \"syntaqx\"."
3897+ },
3898+ {
3899+ "name": "kind",
3900+ "in": "path",
3901+ "required": true,
3902+ "schema": {
3903+ "type": "string",
3904+ "enum": [
3905+ "project",
3906+ "app",
3907+ "api",
3908+ "package",
3909+ "language",
3910+ "owner",
3911+ "environment",
3912+ "integration",
3913+ "doc"
3914+ ]
3915+ }
3916+ },
3917+ {
3918+ "name": "id",
3919+ "in": "path",
3920+ "required": true,
3921+ "schema": {
3922+ "type": "string"
3923+ },
3924+ "description": "Its id (ent_…), or its key: a project's slug, npm:<name>, a username."
3925+ }
3926+ ],
3927+ "responses": {
3928+ "200": {
3929+ "description": "Success.",
3930+ "content": {
3931+ "application/json": {
3932+ "schema": {},
3933+ "example": {
3934+ "entity": {
3935+ "id": "ent_9b1d6f0a2c3e4b5d6e7f8a9b",
3936+ "workspace": "acme",
3937+ "kind": "project",
3938+ "key": "web",
3939+ "name": "web",
3940+ "summary": "The storefront. Written in TypeScript. Packages: @acme/web. Uses api. Owned by ana.",
3941+ "project": "web",
3942+ "private": true,
3943+ "data": {
3944+ "repo": "acme/web",
3945+ "root_dir": "",
3946+ "default_branch": "main",
3947+ "languages": [
3948+ "TypeScript"
3949+ ],
3950+ "owners": [
3951+ "ana"
3952+ ],
3953+ "tests": true,
3954+ "test_commands": [
3955+ "npm test"
3956+ ],
3957+ "production_url": "https://web--acme.g1t.page"
3958+ },
3959+ "source": "scan",
3960+ "ref": "/acme/web",
3961+ "updated_at": "2026-10-04T20:58:41.000Z"
3962+ },
3963+ "relations": [
3964+ {
3965+ "kind": "depends_on",
3966+ "direction": "out",
3967+ "entity": {
3968+ "id": "ent_0a7c3e5b9d1f2a4c6e8b0d2f",
3969+ "workspace": "acme",
3970+ "kind": "project",
3971+ "key": "api",
3972+ "name": "api",
3973+ "summary": "The public API. Written in Rust.",
3974+ "project": "api",
3975+ "private": true,
3976+ "data": {},
3977+ "source": "scan",
3978+ "ref": "/acme/api",
3979+ "updated_at": "2026-10-04T20:57:12.000Z"
3980+ }
3981+ },
3982+ {
3983+ "kind": "owned_by",
3984+ "direction": "out",
3985+ "entity": {
3986+ "id": "ent_3c5e7a9b1d2f4a6c8e0b2d4f",
3987+ "workspace": "acme",
3988+ "kind": "owner",
3989+ "key": "ana",
3990+ "name": "ana",
3991+ "summary": null,
3992+ "project": null,
3993+ "private": false,
3994+ "data": {},
3995+ "source": "scan",
3996+ "ref": "/u/ana",
3997+ "updated_at": "2026-10-04T20:58:41.000Z"
3998+ }
3999+ }
4000+ ]
4001+ }
4002+ }
4003+ }
4004+ },
4005+ "401": {
4006+ "description": "A token is required, or the one sent is not valid.",
4007+ "content": {
4008+ "application/json": {
4009+ "schema": {
4010+ "$ref": "#/components/schemas/Error"
4011+ }
4012+ }
4013+ }
4014+ },
4015+ "403": {
4016+ "description": "Signed in, but not allowed to do this.",
4017+ "content": {
4018+ "application/json": {
4019+ "schema": {
4020+ "$ref": "#/components/schemas/Error"
4021+ }
4022+ }
4023+ }
4024+ },
4025+ "404": {
4026+ "description": "It does not exist, or you cannot see it.",
4027+ "content": {
4028+ "application/json": {
4029+ "schema": {
4030+ "$ref": "#/components/schemas/Error"
4031+ }
4032+ }
4033+ }
4034+ },
4035+ "422": {
4036+ "description": "The input is not valid.",
4037+ "content": {
4038+ "application/json": {
4039+ "schema": {
4040+ "$ref": "#/components/schemas/Error"
4041+ }
4042+ }
4043+ }
4044+ }
4045+ },
4046+ "x-example-params": {
4047+ "workspace": "acme",
4048+ "kind": "project",
4049+ "id": "web"
4050+ }
4051+ }
4052+ },
36874053 "/workspaces/{workspace}/integrations": {
36884054 "get": {
36894055 "operationId": "list_integrations",
36914057 "Integrations"
36924058 ],
36934059 "summary": "List integrations",
3694− "description": "A workspace's integrations: its own model provider, the alert sources that open issues (Sentry, Datadog, webhooks), and the trackers whose tickets agents can read (Jira, Linear). Secrets are never returned. Members only.\n\n`kind` is `models`, `alerts` or `tracker`. `secretHint` shows the end of the secret; the secret itself is never returned. `webhookUrl` is where an alert source sends its alerts.",
4060+ "description": "A workspace's integrations: its own model provider, the alert sources that open issues (Sentry, Datadog, webhooks), and the trackers whose tickets agents can read (Jira, Linear). Secrets are never returned. Members only.\n\n`kind` is `models`, `alerts` or `tracker`. `secret_hint` shows the end of the secret; the secret itself is never returned. `webhook_url` is where an alert source sends its alerts.",
36954061 "x-mcp-tool": "list_integrations",
36964062 "security": [
36974063 {
37244090 "name": "Anthropic",
37254091 "config": {
37264092 "assign": false,
3727− "writeBack": true
4093+ "write_back": true
37284094 },
3729− "secretHint": "…3f9a",
3730− "webhookUrl": null,
3731− "createdBy": "syntaqx",
3732− "createdAt": "2026-10-04T15:42:07.318Z",
3733− "lastUsedAt": "2026-10-04T15:42:08.102Z",
3734− "lastError": null,
4095+ "secret_hint": "…3f9a",
4096+ "webhook_url": null,
4097+ "created_by": "syntaqx",
4098+ "created_at": "2026-10-04T15:42:07.318Z",
4099+ "last_used_at": "2026-10-04T15:42:08.102Z",
4100+ "last_error": null,
37354101 "models": [
37364102 "claude-haiku-4-5",
37374103 "claude-sonnet-4-5"
37454111 "name": "Jira",
37464112 "config": {
37474113 "assign": false,
3748− "writeBack": true,
4114+ "write_back": true,
37494115 "site": "https://acme.atlassian.net",
37504116 "email": "syntaqx@example.com",
37514117 "keys": [
37524118 "TECH"
37534119 ]
37544120 },
3755− "secretHint": "…x7Qe",
3756− "webhookUrl": null,
3757− "createdBy": "syntaqx",
3758− "createdAt": "2026-10-04T15:42:07.318Z",
3759− "lastUsedAt": null,
3760− "lastError": null,
4121+ "secret_hint": "…x7Qe",
4122+ "webhook_url": null,
4123+ "created_by": "syntaqx",
4124+ "created_at": "2026-10-04T15:42:07.318Z",
4125+ "last_used_at": null,
4126+ "last_error": null,
37614127 "models": []
37624128 }
37634129 ]
38154181 "Integrations"
38164182 ],
38174183 "summary": "Connect an integration",
3818− "description": "Connect a workspace to an outside system. provider is a model provider (anthropic, openai, gemini, xai, mistral, deepseek, azure_openai, openrouter, groq, together, fireworks, cerebras, anthropic_endpoint or openai_endpoint: your own key, billed by that provider, and free on g1t while it is being built out; a workspace can connect several and route each kind of work with set_model_routes), or sentry, datadog, webhook, jira or linear. config holds the settings each needs; secret is the API key or token. For datadog and webhook, g1t makes the signing secret and returns it once. Owners only.\n\n`signingSecret` is set only when g1t made it, for `datadog` and `webhook`, and is shown only this once. A model provider is tested as it is connected. See [integrations](/guides/integrations/) and [model providers](/guides/models/).",
4184+ "description": "Connect a workspace to an outside system. provider is a model provider (anthropic, openai, gemini, xai, mistral, deepseek, azure_openai, openrouter, groq, together, fireworks, cerebras, anthropic_endpoint or openai_endpoint: your own key, billed by that provider, and free on g1t while it is being built out; a workspace can connect several and route each kind of work with set_model_routes), or sentry, datadog, webhook, jira or linear. config holds the settings each needs; secret is the API key or token. For datadog and webhook, g1t makes the signing secret and returns it once. Owners only.\n\n`signing_secret` is set only when g1t made it, for `datadog` and `webhook`, and is shown only this once. A model provider is tested as it is connected. See [integrations](/guides/integrations/) and [model providers](/guides/models/).",
38194185 "x-mcp-tool": "connect_integration",
38204186 "security": [
38214187 {
38504216 "repo": "syntaqx/hello",
38514217 "label": "bug",
38524218 "assign": false,
3853− "writeBack": true
4219+ "write_back": true
38544220 },
3855− "secretHint": null,
3856− "webhookUrl": "https://api.g1t.sh/hooks/con_01kpx4n8r3g9s0v5w7x1z2a3bd",
3857− "createdBy": "syntaqx",
3858− "createdAt": "2026-10-04T15:42:07.318Z",
3859− "lastUsedAt": null,
3860− "lastError": null,
4221+ "secret_hint": null,
4222+ "webhook_url": "https://api.g1t.sh/hooks/con_01kpx4n8r3g9s0v5w7x1z2a3bd",
4223+ "created_by": "syntaqx",
4224+ "created_at": "2026-10-04T15:42:07.318Z",
4225+ "last_used_at": null,
4226+ "last_error": null,
38614227 "models": []
38624228 },
3863− "signingSecret": "g1ts_9f2c4a7e1b0d3c6f8a2e5b7d9c1f4a6e8b0d2c4f6a8e1b3d"
4229+ "signing_secret": "g1ts_9f2c4a7e1b0d3c6f8a2e5b7d9c1f4a6e8b0d2c4f6a8e1b3d"
38644230 }
38654231 }
38664232 }
40354401 "pull.merged"
40364402 ],
40374403 "active": true,
4038− "secretHint": "…9c2e",
4039− "createdBy": "syntaqx",
4040− "createdAt": "2026-10-04T15:42:07.318Z",
4041− "lastStatus": "delivered",
4042− "lastDeliveredAt": "2026-10-04T15:42:07.611Z"
4404+ "secret_hint": "…9c2e",
4405+ "created_by": "syntaqx",
4406+ "created_at": "2026-10-04T15:42:07.318Z",
4407+ "last_status": "delivered",
4408+ "last_delivered_at": "2026-10-04T15:42:07.611Z"
40434409 }
40444410 ]
40454411 }
40934459 "Webhooks"
40944460 ],
40954461 "summary": "Create a webhook",
4096− "description": "Register an HTTPS address to be sent events as they happen: a signed JSON POST for each, retried for hours if the receiver does not answer with a 2xx. events lists the event types, or leave it out for all. Without a secret, g1t makes one and returns it once. A ping is sent at once. Members, for a repository; owners, for a workspace.\n\nA ping is sent before the response, so `lastStatus` is usually set already. When g1t made the signing secret, `secret` holds it: it is shown only this once. See [webhooks](/guides/webhooks/).",
4462+ "description": "Register an HTTPS address to be sent events as they happen: a signed JSON POST for each, retried for hours if the receiver does not answer with a 2xx. events lists the event types, or leave it out for all. Without a secret, g1t makes one and returns it once. A ping is sent at once. Members, for a repository; owners, for a workspace.\n\nA ping is sent before the response, so `last_status` is usually set already. When g1t made the signing secret, `secret` holds it: it is shown only this once. See [webhooks](/guides/webhooks/).",
40974463 "x-mcp-tool": "create_webhook",
40984464 "security": [
40994465 {
41384504 "pull.merged"
41394505 ],
41404506 "active": true,
4141− "secretHint": "…9c2e",
4142− "createdBy": "syntaqx",
4143− "createdAt": "2026-10-04T15:42:07.318Z",
4144− "lastStatus": "delivered",
4145− "lastDeliveredAt": "2026-10-04T15:42:07.611Z"
4507+ "secret_hint": "…9c2e",
4508+ "created_by": "syntaqx",
4509+ "created_at": "2026-10-04T15:42:07.318Z",
4510+ "last_status": "delivered",
4511+ "last_delivered_at": "2026-10-04T15:42:07.611Z"
41464512 },
41474513 "secret": "whsec_…"
41484514 }
43264692 "pull.merged"
43274693 ],
43284694 "active": false,
4329− "secretHint": "…9c2e",
4330− "createdBy": "syntaqx",
4331− "createdAt": "2026-10-04T15:42:07.318Z",
4332− "lastStatus": "delivered",
4333− "lastDeliveredAt": "2026-10-04T15:42:07.611Z"
4695+ "secret_hint": "…9c2e",
4696+ "created_by": "syntaqx",
4697+ "created_at": "2026-10-04T15:42:07.318Z",
4698+ "last_status": "delivered",
4699+ "last_delivered_at": "2026-10-04T15:42:07.611Z"
43344700 }
43354701 }
43364702 }
46194985 "schema": {},
46204986 "example": {
46214987 "id": "dlv_01kpx5p9v2j7k3m8n4p5q6r7st",
4622− "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
4623− "eventId": "",
4988+ "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
4989+ "event_id": "",
46244990 "event": "ping",
46254991 "status": "delivered",
46264992 "attempts": 1,
4627− "responseStatus": 200,
4628− "responseBody": "ok",
4993+ "response_status": 200,
4994+ "response_body": "ok",
46294995 "error": null,
4630− "durationMs": 184,
4996+ "duration_ms": 184,
46314997 "payload": "{\"hook\":{\"events\":[\"git.push\",\"pull.merged\"],\"id\":\"hk_01kpx5p9s4h0t1w6x8y2a3b4cd\",\"url\":\"https://hooks.example.com/g1t\"},\"message\":\"g1t will send this webhook's events here.\",\"time\":\"2026-10-04T16:01:12.440Z\",\"type\":\"ping\"}",
4632− "createdAt": "2026-10-04T16:01:12.440Z",
4633− "deliveredAt": "2026-10-04T16:01:12.631Z",
4634− "nextAttemptAt": null
4998+ "created_at": "2026-10-04T16:01:12.440Z",
4999+ "delivered_at": "2026-10-04T16:01:12.631Z",
5000+ "next_attempt_at": null
46355001 }
46365002 }
46375003 }
47155081 "Webhooks"
47165082 ],
47175083 "summary": "List webhook deliveries",
4718− "description": "A webhook's latest deliveries, newest first: what was sent, how the receiver answered, and when it will be tried again.\n\nReturns at most 50, newest first. `status` is `pending`, `delivered` or `failed`; `nextAttemptAt` is set while it is pending.",
5084+ "description": "A webhook's latest deliveries, newest first: what was sent, how the receiver answered, and when it will be tried again.\n\nReturns at most 50, newest first. `status` is `pending`, `delivered` or `failed`; `next_attempt_at` is set while it is pending.",
47195085 "x-mcp-tool": "list_webhook_deliveries",
47205086 "security": [
47215087 {
47605126 "example": [
47615127 {
47625128 "id": "dlv_01kpx6a2b3c4d5e6f7g8h9j0kz",
4763− "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
4764− "eventId": "",
5129+ "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
5130+ "event_id": "",
47655131 "event": "ping",
47665132 "status": "pending",
47675133 "attempts": 1,
4768− "responseStatus": 503,
4769− "responseBody": "Service Unavailable",
5134+ "response_status": 503,
5135+ "response_body": "Service Unavailable",
47705136 "error": null,
4771− "durationMs": 212,
5137+ "duration_ms": 212,
47725138 "payload": "{\"hook\":{\"events\":[\"git.push\",\"pull.merged\"],\"id\":\"hk_01kpx5p9s4h0t1w6x8y2a3b4cd\",\"url\":\"https://hooks.example.com/g1t\"},\"message\":\"g1t will send this webhook's events here.\",\"time\":\"2026-10-04T16:01:12.440Z\",\"type\":\"ping\"}",
4773− "createdAt": "2026-10-04T16:20:03.100Z",
4774− "deliveredAt": null,
4775− "nextAttemptAt": "2026-10-04T16:21:03.312Z"
5139+ "created_at": "2026-10-04T16:20:03.100Z",
5140+ "delivered_at": null,
5141+ "next_attempt_at": "2026-10-04T16:21:03.312Z"
47765142 },
47775143 {
47785144 "id": "dlv_01kpx5p9v2j7k3m8n4p5q6r7st",
4779− "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
4780− "eventId": "",
5145+ "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
5146+ "event_id": "",
47815147 "event": "ping",
47825148 "status": "delivered",
47835149 "attempts": 1,
4784− "responseStatus": 200,
4785− "responseBody": "ok",
5150+ "response_status": 200,
5151+ "response_body": "ok",
47865152 "error": null,
4787− "durationMs": 184,
5153+ "duration_ms": 184,
47885154 "payload": "{\"hook\":{\"events\":[\"git.push\",\"pull.merged\"],\"id\":\"hk_01kpx5p9s4h0t1w6x8y2a3b4cd\",\"url\":\"https://hooks.example.com/g1t\"},\"message\":\"g1t will send this webhook's events here.\",\"time\":\"2026-10-04T16:01:12.440Z\",\"type\":\"ping\"}",
4789− "createdAt": "2026-10-04T16:01:12.440Z",
4790− "deliveredAt": "2026-10-04T16:01:12.631Z",
4791− "nextAttemptAt": null
5155+ "created_at": "2026-10-04T16:01:12.440Z",
5156+ "delivered_at": "2026-10-04T16:01:12.631Z",
5157+ "next_attempt_at": null
47925158 }
47935159 ]
47945160 }
49005266 "schema": {},
49015267 "example": {
49025268 "id": "dlv_01kpx6c8d1e2f3g4h5j6k7m8np",
4903− "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
4904− "eventId": "",
5269+ "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
5270+ "event_id": "",
49055271 "event": "ping",
49065272 "status": "delivered",
49075273 "attempts": 1,
4908− "responseStatus": 200,
4909− "responseBody": "ok",
5274+ "response_status": 200,
5275+ "response_body": "ok",
49105276 "error": null,
4911− "durationMs": 171,
5277+ "duration_ms": 171,
49125278 "payload": "{\"hook\":{\"events\":[\"git.push\",\"pull.merged\"],\"id\":\"hk_01kpx5p9s4h0t1w6x8y2a3b4cd\",\"url\":\"https://hooks.example.com/g1t\"},\"message\":\"g1t will send this webhook's events here.\",\"time\":\"2026-10-04T16:01:12.440Z\",\"type\":\"ping\"}",
4913− "createdAt": "2026-10-04T16:25:40.007Z",
4914− "deliveredAt": "2026-10-04T16:25:40.178Z",
4915− "nextAttemptAt": null
5279+ "created_at": "2026-10-04T16:25:40.007Z",
5280+ "delivered_at": "2026-10-04T16:25:40.178Z",
5281+ "next_attempt_at": null
49165282 }
49175283 }
49185284 }
50335399 "pull.merged"
50345400 ],
50355401 "active": true,
5036− "secretHint": "…9c2e",
5037− "createdBy": "syntaqx",
5038− "createdAt": "2026-10-04T15:42:07.318Z",
5039− "lastStatus": "delivered",
5040− "lastDeliveredAt": "2026-10-04T15:42:07.611Z"
5402+ "secret_hint": "…9c2e",
5403+ "created_by": "syntaqx",
5404+ "created_at": "2026-10-04T15:42:07.318Z",
5405+ "last_status": "delivered",
5406+ "last_delivered_at": "2026-10-04T15:42:07.611Z"
50415407 }
50425408 ]
50435409 }
50945460 "Webhooks"
50955461 ],
50965462 "summary": "Create a webhook for a workspace",
5097− "description": "Register an HTTPS address to be sent events as they happen: a signed JSON POST for each, retried for hours if the receiver does not answer with a 2xx. events lists the event types, or leave it out for all. Without a secret, g1t makes one and returns it once. A ping is sent at once. Members, for a repository; owners, for a workspace.\n\nA ping is sent before the response, so `lastStatus` is usually set already. When g1t made the signing secret, `secret` holds it: it is shown only this once. See [webhooks](/guides/webhooks/).",
5463+ "description": "Register an HTTPS address to be sent events as they happen: a signed JSON POST for each, retried for hours if the receiver does not answer with a 2xx. events lists the event types, or leave it out for all. Without a secret, g1t makes one and returns it once. A ping is sent at once. Members, for a repository; owners, for a workspace.\n\nA ping is sent before the response, so `last_status` is usually set already. When g1t made the signing secret, `secret` holds it: it is shown only this once. See [webhooks](/guides/webhooks/).",
50985464 "x-mcp-tool": "create_webhook",
50995465 "security": [
51005466 {
51295495 "*"
51305496 ],
51315497 "active": true,
5132− "secretHint": "…9c2e",
5133− "createdBy": "syntaqx",
5134− "createdAt": "2026-10-04T15:42:07.318Z",
5135− "lastStatus": "delivered",
5136− "lastDeliveredAt": "2026-10-04T15:42:07.611Z"
5498+ "secret_hint": "…9c2e",
5499+ "created_by": "syntaqx",
5500+ "created_at": "2026-10-04T15:42:07.318Z",
5501+ "last_status": "delivered",
5502+ "last_delivered_at": "2026-10-04T15:42:07.611Z"
51375503 },
51385504 "secret": "whsec_…"
51395505 }
53035669 "pull.merged"
53045670 ],
53055671 "active": false,
5306− "secretHint": "…9c2e",
5307− "createdBy": "syntaqx",
5308− "createdAt": "2026-10-04T15:42:07.318Z",
5309− "lastStatus": "delivered",
5310− "lastDeliveredAt": "2026-10-04T15:42:07.611Z"
5672+ "secret_hint": "…9c2e",
5673+ "created_by": "syntaqx",
5674+ "created_at": "2026-10-04T15:42:07.318Z",
5675+ "last_status": "delivered",
5676+ "last_delivered_at": "2026-10-04T15:42:07.611Z"
53115677 }
53125678 }
53135679 }
55605926 "schema": {},
55615927 "example": {
55625928 "id": "dlv_01kpx5p9v2j7k3m8n4p5q6r7st",
5563− "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
5564− "eventId": "",
5929+ "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
5930+ "event_id": "",
55655931 "event": "ping",
55665932 "status": "delivered",
55675933 "attempts": 1,
5568− "responseStatus": 200,
5569− "responseBody": "ok",
5934+ "response_status": 200,
5935+ "response_body": "ok",
55705936 "error": null,
5571− "durationMs": 184,
5937+ "duration_ms": 184,
55725938 "payload": "{\"hook\":{\"events\":[\"git.push\",\"pull.merged\"],\"id\":\"hk_01kpx5p9s4h0t1w6x8y2a3b4cd\",\"url\":\"https://hooks.example.com/g1t\"},\"message\":\"g1t will send this webhook's events here.\",\"time\":\"2026-10-04T16:01:12.440Z\",\"type\":\"ping\"}",
5573− "createdAt": "2026-10-04T16:01:12.440Z",
5574− "deliveredAt": "2026-10-04T16:01:12.631Z",
5575− "nextAttemptAt": null
5939+ "created_at": "2026-10-04T16:01:12.440Z",
5940+ "delivered_at": "2026-10-04T16:01:12.631Z",
5941+ "next_attempt_at": null
55765942 }
55775943 }
55785944 }
56776043 "example": [
56786044 {
56796045 "id": "dlv_01kpx6a2b3c4d5e6f7g8h9j0kz",
5680− "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
5681− "eventId": "",
6046+ "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
6047+ "event_id": "",
56826048 "event": "ping",
56836049 "status": "pending",
56846050 "attempts": 1,
5685− "responseStatus": 503,
5686− "responseBody": "Service Unavailable",
6051+ "response_status": 503,
6052+ "response_body": "Service Unavailable",
56876053 "error": null,
5688− "durationMs": 212,
6054+ "duration_ms": 212,
56896055 "payload": "{\"hook\":{\"events\":[\"git.push\",\"pull.merged\"],\"id\":\"hk_01kpx5p9s4h0t1w6x8y2a3b4cd\",\"url\":\"https://hooks.example.com/g1t\"},\"message\":\"g1t will send this webhook's events here.\",\"time\":\"2026-10-04T16:01:12.440Z\",\"type\":\"ping\"}",
5690− "createdAt": "2026-10-04T16:20:03.100Z",
5691− "deliveredAt": null,
5692− "nextAttemptAt": "2026-10-04T16:21:03.312Z"
6056+ "created_at": "2026-10-04T16:20:03.100Z",
6057+ "delivered_at": null,
6058+ "next_attempt_at": "2026-10-04T16:21:03.312Z"
56936059 },
56946060 {
56956061 "id": "dlv_01kpx5p9v2j7k3m8n4p5q6r7st",
5696− "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
5697− "eventId": "",
6062+ "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
6063+ "event_id": "",
56986064 "event": "ping",
56996065 "status": "delivered",
57006066 "attempts": 1,
5701− "responseStatus": 200,
5702− "responseBody": "ok",
6067+ "response_status": 200,
6068+ "response_body": "ok",
57036069 "error": null,
5704− "durationMs": 184,
6070+ "duration_ms": 184,
57056071 "payload": "{\"hook\":{\"events\":[\"git.push\",\"pull.merged\"],\"id\":\"hk_01kpx5p9s4h0t1w6x8y2a3b4cd\",\"url\":\"https://hooks.example.com/g1t\"},\"message\":\"g1t will send this webhook's events here.\",\"time\":\"2026-10-04T16:01:12.440Z\",\"type\":\"ping\"}",
5706− "createdAt": "2026-10-04T16:01:12.440Z",
5707− "deliveredAt": "2026-10-04T16:01:12.631Z",
5708− "nextAttemptAt": null
6072+ "created_at": "2026-10-04T16:01:12.440Z",
6073+ "delivered_at": "2026-10-04T16:01:12.631Z",
6074+ "next_attempt_at": null
57096075 }
57106076 ]
57116077 }
58096175 "schema": {},
58106176 "example": {
58116177 "id": "dlv_01kpx6c8d1e2f3g4h5j6k7m8np",
5812− "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
5813− "eventId": "",
6178+ "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd",
6179+ "event_id": "",
58146180 "event": "ping",
58156181 "status": "delivered",
58166182 "attempts": 1,
5817− "responseStatus": 200,
5818− "responseBody": "ok",
6183+ "response_status": 200,
6184+ "response_body": "ok",
58196185 "error": null,
5820− "durationMs": 171,
6186+ "duration_ms": 171,
58216187 "payload": "{\"hook\":{\"events\":[\"git.push\",\"pull.merged\"],\"id\":\"hk_01kpx5p9s4h0t1w6x8y2a3b4cd\",\"url\":\"https://hooks.example.com/g1t\"},\"message\":\"g1t will send this webhook's events here.\",\"time\":\"2026-10-04T16:01:12.440Z\",\"type\":\"ping\"}",
5822− "createdAt": "2026-10-04T16:25:40.007Z",
5823− "deliveredAt": "2026-10-04T16:25:40.178Z",
5824− "nextAttemptAt": null
6188+ "created_at": "2026-10-04T16:25:40.007Z",
6189+ "delivered_at": "2026-10-04T16:25:40.178Z",
6190+ "next_attempt_at": null
58256191 }
58266192 }
58276193 }
59186284 "example": [
59196285 {
59206286 "task": "default",
5921− "connectionId": "con_01kpx3m7q2f8r9t4v6w0y1z2ab",
6287+ "connection_id": "con_01kpx3m7q2f8r9t4v6w0y1z2ab",
59226288 "model": "claude-sonnet-4-5"
59236289 },
59246290 {
59256291 "task": "review",
5926− "connectionId": null,
6292+ "connection_id": null,
59276293 "model": null
59286294 }
59296295 ]
60086374 "example": [
60096375 {
60106376 "task": "default",
6011− "connectionId": "con_01kpx3m7q2f8r9t4v6w0y1z2ab",
6377+ "connection_id": "con_01kpx3m7q2f8r9t4v6w0y1z2ab",
60126378 "model": "claude-sonnet-4-5"
60136379 },
60146380 {
60156381 "task": "review",
6016− "connectionId": null,
6382+ "connection_id": null,
60176383 "model": null
60186384 }
60196385 ]
64106776 "default": false
64116777 }
64126778 },
6413− "lastRun": {
6779+ "last_run": {
64146780 "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
6415− "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
6781+ "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
64166782 "path": ".g1t/workflows/ci.yml",
64176783 "name": "CI",
64186784 "title": "Greeting should name the caller",
64266792 "conclusion": "success",
64276793 "error": null,
64286794 "actor": "syntaqx",
6429− "createdAt": "2026-10-04T15:42:07.318Z",
6430− "startedAt": "2026-10-04T15:42:09.020Z",
6431− "finishedAt": "2026-10-04T15:44:31.877Z"
6795+ "created_at": "2026-10-04T15:42:07.318Z",
6796+ "started_at": "2026-10-04T15:42:09.020Z",
6797+ "finished_at": "2026-10-04T15:44:31.877Z"
64326798 }
64336799 },
64346800 {
64426808 "error": null,
64436809 "notes": [],
64446810 "dispatch": null,
6445− "lastRun": null
6811+ "last_run": null
64466812 }
64476813 ]
64486814 }
65696935 "example": [
65706936 {
65716937 "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
6572− "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
6938+ "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
65736939 "path": ".g1t/workflows/ci.yml",
65746940 "name": "CI",
65756941 "title": "Greeting should name the caller",
65836949 "conclusion": "success",
65846950 "error": null,
65856951 "actor": "syntaqx",
6586− "createdAt": "2026-10-04T15:42:07.318Z",
6587− "startedAt": "2026-10-04T15:42:09.020Z",
6588− "finishedAt": "2026-10-04T15:44:31.877Z"
6952+ "created_at": "2026-10-04T15:42:07.318Z",
6953+ "started_at": "2026-10-04T15:42:09.020Z",
6954+ "finished_at": "2026-10-04T15:44:31.877Z"
65896955 }
65906956 ]
65916957 }
66927058 "schema": {},
66937059 "example": {
66947060 "id": "run_01kpx8d4e7f0g3h6j9k2m5n8pq",
6695− "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
7061+ "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
66967062 "path": ".g1t/workflows/ci.yml",
66977063 "name": "CI",
66987064 "title": "CI run by syntaqx",
67067072 "conclusion": null,
67077073 "error": null,
67087074 "actor": "syntaqx",
6709− "createdAt": "2026-10-04T16:30:00.512Z",
6710− "startedAt": null,
6711− "finishedAt": null
7075+ "created_at": "2026-10-04T16:30:00.512Z",
7076+ "started_at": null,
7077+ "finished_at": null
67127078 }
67137079 }
67147080 }
68557221 "error": null,
68567222 "notes": [],
68577223 "dispatch": null,
6858− "lastRun": null
7224+ "last_run": null
68597225 }
68607226 }
68617227 }
69977363 "error": null,
69987364 "notes": [],
69997365 "dispatch": null,
7000− "lastRun": null
7366+ "last_run": null
70017367 }
70027368 }
70037369 }
71367502 "error": null,
71377503 "notes": [],
71387504 "dispatch": null,
7139− "lastRun": null
7505+ "last_run": null
71407506 }
71417507 }
71427508 }
73117677 "example": [
73127678 {
73137679 "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
7314− "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
7680+ "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
73157681 "path": ".g1t/workflows/ci.yml",
73167682 "name": "CI",
73177683 "title": "Greeting should name the caller",
73257691 "conclusion": "success",
73267692 "error": null,
73277693 "actor": "syntaqx",
7328− "createdAt": "2026-10-04T15:42:07.318Z",
7329− "startedAt": "2026-10-04T15:42:09.020Z",
7330− "finishedAt": "2026-10-04T15:44:31.877Z"
7694+ "created_at": "2026-10-04T15:42:07.318Z",
7695+ "started_at": "2026-10-04T15:42:09.020Z",
7696+ "finished_at": "2026-10-04T15:44:31.877Z"
73317697 }
73327698 ]
73337699 }
74337799 "example": {
74347800 "run": {
74357801 "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
7436− "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
7802+ "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
74377803 "path": ".g1t/workflows/ci.yml",
74387804 "name": "CI",
74397805 "title": "Greeting should name the caller",
74477813 "conclusion": "failure",
74487814 "error": null,
74497815 "actor": "syntaqx",
7450− "createdAt": "2026-10-04T15:42:07.318Z",
7451− "startedAt": "2026-10-04T15:42:09.020Z",
7452− "finishedAt": "2026-10-04T15:44:31.877Z"
7816+ "created_at": "2026-10-04T15:42:07.318Z",
7817+ "started_at": "2026-10-04T15:42:09.020Z",
7818+ "finished_at": "2026-10-04T15:44:31.877Z"
74537819 },
74547820 "jobs": [
74557821 {
74567822 "id": "job_01kpx7b3d0e4f8g2h6j0k4m8ns",
7457− "runId": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
7823+ "run_id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
74587824 "key": "test",
74597825 "name": "test",
74607826 "needs": [],
74667832 "name": "Run actions/checkout@v4",
74677833 "status": "completed",
74687834 "conclusion": "success",
7469− "startedAt": "2026-10-04T15:42:10.101Z",
7470− "finishedAt": "2026-10-04T15:42:12.433Z"
7835+ "started_at": "2026-10-04T15:42:10.101Z",
7836+ "finished_at": "2026-10-04T15:42:12.433Z"
74717837 },
74727838 {
74737839 "number": 2,
74747840 "name": "Run cargo test",
74757841 "status": "completed",
74767842 "conclusion": "failure",
7477− "startedAt": "2026-10-04T15:42:12.440Z",
7478− "finishedAt": "2026-10-04T15:44:30.902Z"
7843+ "started_at": "2026-10-04T15:42:12.440Z",
7844+ "finished_at": "2026-10-04T15:44:30.902Z"
74797845 }
74807846 ],
74817847 "annotations": [
74887854 }
74897855 ],
74907856 "reason": null,
7491− "startedAt": "2026-10-04T15:42:09.020Z",
7492− "finishedAt": "2026-10-04T15:44:31.002Z"
7857+ "started_at": "2026-10-04T15:42:09.020Z",
7858+ "finished_at": "2026-10-04T15:44:31.002Z"
74937859 },
74947860 {
74957861 "id": "job_01kpx7b3d0e4f8g2h6j0k4m8nt",
7496− "runId": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
7862+ "run_id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
74977863 "key": "deploy",
74987864 "name": "deploy",
74997865 "needs": [
75047870 "steps": [],
75057871 "annotations": [],
75067872 "reason": "A job it needs did not succeed.",
7507− "startedAt": null,
7508− "finishedAt": "2026-10-04T15:44:31.877Z"
7873+ "started_at": null,
7874+ "finished_at": "2026-10-04T15:44:31.877Z"
75097875 }
75107876 ],
75117877 "notes": [
76167982 "schema": {},
76177983 "example": {
76187984 "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
7619− "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
7985+ "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
76207986 "path": ".g1t/workflows/ci.yml",
76217987 "name": "CI",
76227988 "title": "Greeting should name the caller",
76307996 "conclusion": "cancelled",
76317997 "error": null,
76327998 "actor": "syntaqx",
7633− "createdAt": "2026-10-04T15:42:07.318Z",
7634− "startedAt": "2026-10-04T15:42:09.020Z",
7635− "finishedAt": "2026-10-04T15:44:31.877Z"
7999+ "created_at": "2026-10-04T15:42:07.318Z",
8000+ "started_at": "2026-10-04T15:42:09.020Z",
8001+ "finished_at": "2026-10-04T15:44:31.877Z"
76368002 }
76378003 }
76388004 }
77448110 "schema": {},
77458111 "example": {
77468112 "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
7747− "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
8113+ "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
77488114 "path": ".g1t/workflows/ci.yml",
77498115 "name": "CI",
77508116 "title": "Greeting should name the caller",
77588124 "conclusion": null,
77598125 "error": null,
77608126 "actor": "syntaqx",
7761− "createdAt": "2026-10-04T15:42:07.318Z",
7762− "startedAt": null,
7763− "finishedAt": null
8127+ "created_at": "2026-10-04T15:42:07.318Z",
8128+ "started_at": null,
8129+ "finished_at": null
77648130 }
77658131 }
77668132 }
78918257 "schema": {},
78928258 "example": {
78938259 "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr",
7894− "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
8260+ "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np",
78958261 "path": ".g1t/workflows/ci.yml",
78968262 "name": "CI",
78978263 "title": "Greeting should name the caller",
79058271 "conclusion": null,
79068272 "error": null,
79078273 "actor": "syntaqx",
7908− "createdAt": "2026-10-04T15:42:07.318Z",
7909− "startedAt": null,
7910− "finishedAt": null
8274+ "created_at": "2026-10-04T15:42:07.318Z",
8275+ "started_at": null,
8276+ "finished_at": null
79118277 }
79128278 }
79138279 }
81568522 "kind": "secret",
81578523 "value": null,
81588524 "scope": "workspace",
8159− "updatedAt": "2026-10-01T09:12:44.020Z",
8160− "availableTo": [
8525+ "updated_at": "2026-10-01T09:12:44.020Z",
8526+ "available_to": [
81618527 "workflows"
81628528 ],
81638529 "environments": [],
81648530 "projects": [],
81658531 "note": null,
8166− "updatedBy": "syntaqx"
8532+ "updated_by": "syntaqx"
81678533 },
81688534 {
81698535 "id": "set_01kpv9q4r7s0t3v6w9x2y5z8ac",
81718537 "kind": "secret",
81728538 "value": null,
81738539 "scope": "project",
8174− "updatedAt": "2026-10-04T15:42:07.318Z",
8175− "availableTo": [
8540+ "updated_at": "2026-10-04T15:42:07.318Z",
8541+ "available_to": [
81768542 "workflows",
81778543 "deployments"
81788544 ],
81818547 ],
81828548 "projects": [],
81838549 "note": "Rotate in the Stripe dashboard.",
8184− "updatedBy": "syntaqx"
8550+ "updated_by": "syntaqx"
81858551 }
81868552 ]
81878553 }
82858651 "kind": "secret",
82868652 "value": null,
82878653 "scope": "project",
8288− "updatedAt": "2026-10-04T15:42:07.318Z",
8289− "availableTo": [
8654+ "updated_at": "2026-10-04T15:42:07.318Z",
8655+ "available_to": [
82908656 "workflows",
82918657 "deployments"
82928658 ],
82958661 ],
82968662 "projects": [],
82978663 "note": null,
8298− "updatedBy": "syntaqx"
8664+ "updated_by": "syntaqx"
82998665 }
83008666 }
83018667 }
85938959 "kind": "variable",
85948960 "value": "20",
85958961 "scope": "project",
8596− "updatedAt": "2026-10-04T15:42:07.318Z",
8597− "availableTo": [
8962+ "updated_at": "2026-10-04T15:42:07.318Z",
8963+ "available_to": [
85988964 "workflows",
85998965 "deployments"
86008966 ],
86018967 "environments": [],
86028968 "projects": [],
86038969 "note": null,
8604− "updatedBy": "syntaqx"
8970+ "updated_by": "syntaqx"
86058971 }
86068972 ]
86078973 }
86949060 "kind": "variable",
86959061 "value": "20",
86969062 "scope": "project",
8697− "updatedAt": "2026-10-04T15:42:07.318Z",
8698− "availableTo": [
9063+ "updated_at": "2026-10-04T15:42:07.318Z",
9064+ "available_to": [
86999065 "workflows",
87009066 "deployments"
87019067 ],
87029068 "environments": [],
87039069 "projects": [],
87049070 "note": null,
8705− "updatedBy": "syntaqx"
9071+ "updated_by": "syntaqx"
87069072 }
87079073 }
87089074 }
88799245 "kind": "variable",
88809246 "value": "22",
88819247 "scope": "project",
8882− "updatedAt": "2026-10-04T15:42:07.318Z",
8883− "availableTo": [
9248+ "updated_at": "2026-10-04T15:42:07.318Z",
9249+ "available_to": [
88849250 "workflows",
88859251 "deployments"
88869252 ],
88879253 "environments": [],
88889254 "projects": [],
88899255 "note": null,
8890− "updatedBy": "syntaqx"
9256+ "updated_by": "syntaqx"
88919257 }
88929258 }
88939259 }
91699535 "kind": "secret",
91709536 "value": null,
91719537 "scope": "workspace",
9172− "updatedAt": "2026-10-04T15:42:07.318Z",
9173− "availableTo": [
9538+ "updated_at": "2026-10-04T15:42:07.318Z",
9539+ "available_to": [
91749540 "workflows"
91759541 ],
91769542 "environments": [],
91779543 "projects": [],
91789544 "note": null,
9179− "updatedBy": "syntaqx"
9545+ "updated_by": "syntaqx"
91809546 }
91819547 ]
91829548 }
92749640 "kind": "secret",
92759641 "value": null,
92769642 "scope": "workspace",
9277− "updatedAt": "2026-10-04T15:42:07.318Z",
9278− "availableTo": [
9643+ "updated_at": "2026-10-04T15:42:07.318Z",
9644+ "available_to": [
92799645 "workflows"
92809646 ],
92819647 "environments": [],
92839649 "hello"
92849650 ],
92859651 "note": null,
9286− "updatedBy": "syntaqx"
9652+ "updated_by": "syntaqx"
92879653 }
92889654 }
92899655 }
95539919 "kind": "variable",
95549920 "value": "20",
95559921 "scope": "workspace",
9556− "updatedAt": "2026-10-04T15:42:07.318Z",
9557− "availableTo": [
9922+ "updated_at": "2026-10-04T15:42:07.318Z",
9923+ "available_to": [
95589924 "workflows",
95599925 "deployments"
95609926 ],
95619927 "environments": [],
95629928 "projects": [],
95639929 "note": null,
9564− "updatedBy": "syntaqx"
9930+ "updated_by": "syntaqx"
95659931 }
95669932 ]
95679933 }
964810014 "kind": "variable",
964910015 "value": "20",
965010016 "scope": "workspace",
9651− "updatedAt": "2026-10-04T15:42:07.318Z",
9652− "availableTo": [
10017+ "updated_at": "2026-10-04T15:42:07.318Z",
10018+ "available_to": [
965310019 "workflows",
965410020 "deployments"
965510021 ],
965610022 "environments": [],
965710023 "projects": [],
965810024 "note": null,
9659− "updatedBy": "syntaqx"
10025+ "updated_by": "syntaqx"
966010026 }
966110027 }
966210028 }
982310189 "kind": "variable",
982410190 "value": "22",
982510191 "scope": "workspace",
9826− "updatedAt": "2026-10-04T15:42:07.318Z",
9827− "availableTo": [
10192+ "updated_at": "2026-10-04T15:42:07.318Z",
10193+ "available_to": [
982810194 "workflows",
982910195 "deployments"
983010196 ],
983110197 "environments": [],
983210198 "projects": [],
983310199 "note": null,
9834− "updatedBy": "syntaqx"
10200+ "updated_by": "syntaqx"
983510201 }
983610202 }
983710203 }
1010110467 "application/json": {
1010210468 "schema": {},
1010310469 "example": {
10104− "planId": "pln_01m43s9c4e8g2j6m0q4t8x2b6d"
10470+ "plan_id": "pln_01m43s9c4e8g2j6m0q4t8x2b6d"
1010510471 }
1010610472 }
1010710473 }
1024210608 "schema": {},
1024310609 "example": {
1024410610 "id": "pln_01m43s9c4e8g2j6m0q4t8x2b6d",
10245− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
10611+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
1024610612 "brief": "Greet people by name, from the command line and the web page.",
1024710613 "status": "ready",
1024810614 "summary": "Name parsing first, then the two places that print the greeting.",
1025910625 "files": [
1026010626 "src/greet.rs"
1026110627 ],
10262− "dependsOn": [],
10628+ "depends_on": [],
1026310629 "number": null
1026410630 },
1026510631 {
1027410640 "files": [
1027510641 "src/web.rs"
1027610642 ],
10277− "dependsOn": [
10643+ "depends_on": [
1027810644 1
1027910645 ],
1028010646 "number": null
1028810654 "verified": false,
1028910655 "workspaces": []
1029010656 },
10291− "createdAt": "2026-10-01T17:58:40.006Z",
10292− "finishedAt": "2026-10-01T18:01:12.774Z",
10657+ "created_at": "2026-10-01T17:58:40.006Z",
10658+ "finished_at": "2026-10-01T18:01:12.774Z",
1029310659 "progress": [],
1029410660 "exchanges": []
1029510661 }
1039310759 "schema": {},
1039410760 "example": {
1039510761 "id": "pln_01m43s9c4e8g2j6m0q4t8x2b6d",
10396− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
10762+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
1039710763 "brief": "Greet people by name, from the command line and the web page.",
1039810764 "status": "applied",
1039910765 "summary": "Name parsing first, then the two places that print the greeting.",
1041010776 "files": [
1041110777 "src/greet.rs"
1041210778 ],
10413− "dependsOn": [],
10779+ "depends_on": [],
1041410780 "number": 12
1041510781 },
1041610782 {
1042510791 "files": [
1042610792 "src/web.rs"
1042710793 ],
10428− "dependsOn": [
10794+ "depends_on": [
1042910795 1
1043010796 ],
1043110797 "number": 13
1043910805 "verified": false,
1044010806 "workspaces": []
1044110807 },
10442− "createdAt": "2026-10-01T17:58:40.006Z",
10443− "finishedAt": "2026-10-01T18:01:12.774Z",
10808+ "created_at": "2026-10-01T17:58:40.006Z",
10809+ "finished_at": "2026-10-01T18:01:12.774Z",
1044410810 "progress": [],
1044510811 "exchanges": []
1044610812 }
1060710973 "path": null,
1060810974 "line": null,
1060910975 "verdict": null,
10610− "createdAt": "2026-10-01T18:12:30.551Z"
10976+ "created_at": "2026-10-01T18:12:30.551Z"
1061110977 }
1061210978 }
1061310979 }
1070211068 "Pull requests"
1070311069 ],
1070411070 "summary": "List pull requests",
10705− "description": "Pull requests on a repository, newest first. State open covers drafts and those ready for review; closed covers merged and closed.\n\nReturns at most 100 pull requests, newest first. `checkStatus` is `queued`, `running`, `passed`, `failed`, `errored`, or `null` when no checks have run against the head.",
11071+ "description": "Pull requests on a repository, newest first. State open covers drafts and those ready for review; closed covers merged and closed.\n\nReturns at most 100 pull requests, newest first. `check_status` is `queued`, `running`, `passed`, `failed`, `errored`, or `null` when no checks have run against the head.",
1070611072 "x-mcp-tool": "list_pull_requests",
1070711073 "security": [
1070811074 {
1075111117 "example": [
1075211118 {
1075311119 "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
10754− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
11120+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
1075511121 "number": 14,
1075611122 "issue": 12,
1075711123 "title": "Greeting should name the caller",
1076311129 "namespace": "pulls",
1076411130 "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
1076511131 },
10766− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
11132+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
1076711133 "branch": null,
10768− "headCommit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
10769− "mergeBase": null,
10770− "mergedBy": null,
10771− "mergedAt": null,
10772− "supersededBy": null,
10773− "checkStatus": "passed",
11134+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
11135+ "merge_base": null,
11136+ "merged_by": null,
11137+ "merged_at": null,
11138+ "superseded_by": null,
11139+ "check_status": "passed",
1077411140 "files": [
1077511141 {
1077611142 "path": "src/main.rs",
1078911155 "verified": false,
1079011156 "workspaces": []
1079111157 },
10792− "createdAt": "2026-10-01T18:20:02.117Z",
10793− "updatedAt": "2026-10-01T18:35:44.902Z"
11158+ "created_at": "2026-10-01T18:20:02.117Z",
11159+ "updated_at": "2026-10-01T18:35:44.902Z"
1079411160 }
1079511161 ]
1079611162 }
1088311249 "example": {
1088411250 "pull": {
1088511251 "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
10886− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
11252+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
1088711253 "number": 14,
1088811254 "issue": 12,
1088911255 "title": "Greeting should name the caller",
1089511261 "namespace": "pulls",
1089611262 "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
1089711263 },
10898− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
11264+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
1089911265 "branch": null,
10900− "headCommit": null,
10901− "mergeBase": null,
10902− "mergedBy": null,
10903− "mergedAt": null,
10904− "supersededBy": null,
10905− "checkStatus": null,
11266+ "head_commit": null,
11267+ "merge_base": null,
11268+ "merged_by": null,
11269+ "merged_at": null,
11270+ "superseded_by": null,
11271+ "check_status": null,
1090611272 "files": [],
1090711273 "assignees": [],
1090811274 "reviewers": [],
1091311279 "verified": false,
1091411280 "workspaces": []
1091511281 },
10916− "createdAt": "2026-10-01T18:20:02.117Z",
10917− "updatedAt": "2026-10-01T18:20:02.117Z"
11282+ "created_at": "2026-10-01T18:20:02.117Z",
11283+ "updated_at": "2026-10-01T18:20:02.117Z"
1091811284 },
1091911285 "git": {
1092011286 "remote": "https://g1t.sh/pulls/pr_01m43smh3vexsr5pmp60qwv0vs.git",
1106711433 "example": {
1106811434 "pull": {
1106911435 "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
11070− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
11436+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
1107111437 "number": 14,
1107211438 "issue": 12,
1107311439 "title": "Greeting should name the caller",
1107911445 "namespace": "pulls",
1108011446 "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
1108111447 },
11082− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
11448+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
1108311449 "branch": null,
11084− "headCommit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
11085− "mergeBase": null,
11086− "mergedBy": null,
11087− "mergedAt": null,
11088− "supersededBy": null,
11089− "checkStatus": "passed",
11450+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
11451+ "merge_base": null,
11452+ "merged_by": null,
11453+ "merged_at": null,
11454+ "superseded_by": null,
11455+ "check_status": "passed",
1109011456 "files": [
1109111457 {
1109211458 "path": "src/main.rs",
1110511471 "verified": false,
1110611472 "workspaces": []
1110711473 },
11108− "createdAt": "2026-10-01T18:20:02.117Z",
11109− "updatedAt": "2026-10-01T18:35:44.902Z"
11474+ "created_at": "2026-10-01T18:20:02.117Z",
11475+ "updated_at": "2026-10-01T18:35:44.902Z"
1111011476 },
1111111477 "issue": {
1111211478 "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
11113− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
11479+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
1111411480 "number": 12,
1111511481 "title": "Greeting should name the caller",
1111611482 "body": "Take a name from the first argument; fall back to world.",
1112211488 ],
1112311489 "state": "open",
1112411490 "reason": null,
11125− "resolvedBy": null,
11491+ "resolved_by": null,
1112611492 "author": {
1112711493 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
1112811494 "username": "syntaqx",
1113011496 "verified": false,
1113111497 "workspaces": []
1113211498 },
11133− "createdAt": "2026-10-01T18:04:11.482Z",
11134− "updatedAt": "2026-10-01T18:20:02.117Z",
11135− "closedAt": null,
11136− "pullCount": 1,
11137− "commentCount": 0,
11499+ "created_at": "2026-10-01T18:04:11.482Z",
11500+ "updated_at": "2026-10-01T18:20:02.117Z",
11501+ "closed_at": null,
11502+ "pull_count": 1,
11503+ "comment_count": 0,
1113811504 "assignees": [],
11139− "blockedBy": [],
11505+ "blocked_by": [],
1114011506 "queued": false,
1114111507 "agent": "claude-code"
1114211508 },
1115511521 "path": null,
1115611522 "line": null,
1115711523 "verdict": null,
11158− "createdAt": "2026-10-01T18:33:10.420Z"
11524+ "created_at": "2026-10-01T18:33:10.420Z"
1115911525 }
1116011526 ],
1116111527 "checks": {
1116211528 "id": "chk_01m43sw8e2g6j0m4q8t2x6a0c4",
11163− "headCommit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
11529+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
1116411530 "status": "passed",
1116511531 "results": [
1116611532 {
1116711533 "command": "cargo test",
1116811534 "passed": true,
11169− "exitCode": 0,
11535+ "exit_code": 0,
1117011536 "output": "test result: ok. 3 passed; 0 failed",
11171− "durationMs": 18234
11537+ "duration_ms": 18234
1117211538 }
1117311539 ],
1117411540 "error": null,
11175− "createdAt": "2026-10-01T18:33:11.002Z",
11176− "finishedAt": "2026-10-01T18:35:44.902Z"
11541+ "created_at": "2026-10-01T18:33:11.002Z",
11542+ "finished_at": "2026-10-01T18:35:44.902Z"
1117711543 },
1117811544 "overlaps": [],
1117911545 "behind": false,
1118611552 "id": "msg_01m43tx5egeh4t3f9zcnjenqvz",
1118711553 "author": "syntaqx",
1118811554 "body": "Keep \"world\" as the default when no name is given.",
11189− "createdAt": "2026-10-01T18:25:00.310Z",
11190− "deliveredAt": "2026-10-01T18:25:31.007Z",
11555+ "created_at": "2026-10-01T18:25:00.310Z",
11556+ "delivered_at": "2026-10-01T18:25:31.007Z",
1119111557 "kind": "message",
11192− "fromNumber": null,
11193− "toNumber": 14,
11558+ "from_number": null,
11559+ "to_number": 14,
1119411560 "answer": null,
1119511561 "declined": false
1119611562 }
1145111817 "path": null,
1145211818 "line": null,
1145311819 "verdict": "request_changes",
11454− "createdAt": "2026-10-01T18:40:05.019Z"
11820+ "created_at": "2026-10-01T18:40:05.019Z"
1145511821 }
1145611822 }
1145711823 }
1187912245 "schema": {},
1188012246 "example": {
1188112247 "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
11882− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
12248+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
1188312249 "number": 14,
1188412250 "issue": 12,
1188512251 "title": "Greeting should name the caller",
1189112257 "namespace": "pulls",
1189212258 "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
1189312259 },
11894− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
12260+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
1189512261 "branch": null,
11896− "headCommit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
11897− "mergeBase": null,
11898− "mergedBy": null,
11899− "mergedAt": null,
11900− "supersededBy": null,
11901− "checkStatus": null,
12262+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
12263+ "merge_base": null,
12264+ "merged_by": null,
12265+ "merged_at": null,
12266+ "superseded_by": null,
12267+ "check_status": null,
1190212268 "files": [
1190312269 {
1190412270 "path": "src/main.rs",
1191512281 "verified": false,
1191612282 "workspaces": []
1191712283 },
11918− "createdAt": "2026-10-01T18:20:02.117Z",
11919− "updatedAt": "2026-10-01T18:33:10.398Z"
12284+ "created_at": "2026-10-01T18:20:02.117Z",
12285+ "updated_at": "2026-10-01T18:33:10.398Z"
1192012286 }
1192112287 }
1192212288 }
1204712413 "schema": {},
1204812414 "example": {
1204912415 "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
12050− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
12416+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
1205112417 "number": 14,
1205212418 "issue": 12,
1205312419 "title": "Greeting should name the caller",
1205912425 "namespace": "pulls",
1206012426 "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
1206112427 },
12062− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
12428+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
1206312429 "branch": null,
12064− "headCommit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
12065− "mergeBase": null,
12066− "mergedBy": null,
12067− "mergedAt": null,
12068− "supersededBy": null,
12069− "checkStatus": null,
12430+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
12431+ "merge_base": null,
12432+ "merged_by": null,
12433+ "merged_at": null,
12434+ "superseded_by": null,
12435+ "check_status": null,
1207012436 "files": [
1207112437 {
1207212438 "path": "src/main.rs",
1208312449 "verified": false,
1208412450 "workspaces": []
1208512451 },
12086− "createdAt": "2026-10-01T18:20:02.117Z",
12087− "updatedAt": "2026-10-01T19:02:48.760Z"
12452+ "created_at": "2026-10-01T18:20:02.117Z",
12453+ "updated_at": "2026-10-01T19:02:48.760Z"
1208812454 }
1208912455 }
1209012456 }
1214912515 "Pull requests"
1215012516 ],
1215112517 "summary": "Merge a pull request",
12152− "description": "Land a pull request on the repository's main branch. Only members of the repository's workspace can merge, and only once it is marked ready and its acceptance checks have passed. Merging resolves the issue it was made for: the issue closes recording this pull request, and the other pull requests still in progress for that issue close as superseded. Where the repository has a merge queue, it joins the queue instead of landing at once. If main has moved since the pull request was opened, it is brought up to date first and lands when that is done; a repository that requires pull requests to be up to date refuses instead, so pull main into its fork or branch, push, and merge again. Check status in the result to see whether it has landed.\n\nA draft, or one whose checks have not passed, answers `409`. A pull request still `open` in the response has not landed yet: `landing` is true on it while it is brought up to date, and [get the merge queue](/reference/api/pull-requests/get-merge-queue/) shows it waiting in the [merge queue](/guides/merge-queue/). Merging records the pull request in the issue's `resolvedBy` and sets `supersededBy` on the pull requests it closes.",
12518+ "description": "Land a pull request on the repository's main branch. Only members of the repository's workspace can merge, and only once it is marked ready and its acceptance checks have passed. Merging resolves the issue it was made for: the issue closes recording this pull request, and the other pull requests still in progress for that issue close as superseded. Where the repository has a merge queue, it joins the queue instead of landing at once. If main has moved since the pull request was opened, it is brought up to date first and lands when that is done; a repository that requires pull requests to be up to date refuses instead, so pull main into its fork or branch, push, and merge again. Check status in the result to see whether it has landed.\n\nA draft, or one whose checks have not passed, answers `409`. A pull request still `open` in the response has not landed yet: `landing` is true on it while it is brought up to date, and [get the merge queue](/reference/api/pull-requests/get-merge-queue/) shows it waiting in the [merge queue](/guides/merge-queue/). Merging records the pull request in the issue's `resolved_by` and sets `superseded_by` on the pull requests it closes.",
1215312519 "x-mcp-tool": "merge_pull_request",
1215412520 "security": [
1215512521 {
1219312559 "schema": {},
1219412560 "example": {
1219512561 "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
12196− "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
12562+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
1219712563 "number": 14,
1219812564 "issue": 12,
1219912565 "title": "Greeting should name the caller",
1220512571 "namespace": "pulls",
1220612572 "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
1220712573 },
12208− "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
12574+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
1220912575 "branch": null,
12210− "headCommit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
12211− "mergeBase": "3a7e9c1b5d2f4a6c8e0b2d4f6a8c0e2b4d6f8a1c",
12212− "mergedBy": "syntaqx",
12213− "mergedAt": "2026-10-01T18:52:17.093Z",
12214− "supersededBy": null,
12215− "checkStatus": "passed",
12576+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
12577+ "merge_base": "3a7e9c1b5d2f4a6c8e0b2d4f6a8c0e2b4d6f8a1c",
12578+ "merged_by": "syntaqx",
12579+ "merged_at": "2026-10-01T18:52:17.093Z",
12580+ "superseded_by": null,
12581+ "check_status": "passed",
1221612582 "files": [
1221712583 {
1221812584 "path": "src/main.rs",
1223112597 "verified": false,
1223212598 "workspaces": []
1223312599 },
12234− "createdAt": "2026-10-01T18:20:02.117Z",
12235− "updatedAt": "2026-10-01T18:52:17.093Z"
12600+ "created_at": "2026-10-01T18:20:02.117Z",
12601+ "updated_at": "2026-10-01T18:52:17.093Z"
1223612602 }
1223712603 }
1223812604 }
+3−1
116116 background: var(--g1t-surface);
117117 color: var(--g1t-fg);
118118 }
119−/* The current page: brighter text and a lavender mark, not a filled bar. */
119+/* The current page: brighter text and a lavender mark, not a filled bar.
120+ Square on the mark's side, so the mark is a straight line. */
120121 .sidebar-content a[aria-current='page'],
121122 .sidebar-content a[aria-current='page']:hover {
123+ border-radius: 0 0.375rem 0.375rem 0;
122124 background: var(--g1t-surface);
123125 color: var(--g1t-fg);
124126 font-weight: 500;
+95−0
1+import { FileText, ScrollText, ShieldCheck } from "lucide-react";
2+import { Link } from "react-router";
3+
4+import type { RepoInstructions } from "@g1t/contracts";
5+
6+import { Idle } from "./agents";
7+import { TimeAgo } from "./ui";
8+
9+const ROLE: Record<RepoInstructions["files"][number]["role"], string> = {
10+ root: "Every run",
11+ directory: "Runs that touch this directory",
12+ review: "Reviews",
13+};
14+
15+/**
16+ * The files every g1t agent run in a project reads as the repository's
17+ * instructions, as they are on its default branch: which, what they say,
18+ * when they last changed, and where to change them.
19+ */
20+export function AgentInstructions({ instructions, base }: { instructions: RepoInstructions; base: string }) {
21+ const { branch, files, limits } = instructions;
22+ return (
23+ <section className="mt-10">
24+ <div className="flex items-baseline justify-between">
25+ <h3 className="flex items-center gap-2 text-sm font-medium">
26+ <ScrollText size={15} className="text-muted" />
27+ Instructions
28+ </h3>
29+ <span className="text-xs text-muted">
30+ {files.length === 0 ? "None yet" : `${files.length} ${files.length === 1 ? "file" : "files"} on ${branch}`}
31+ </span>
32+ </div>
33+ <p className="mt-1.5 max-w-2xl text-sm text-muted">
34+ Every agent run here reads the repository's <code className="font-mono text-xs">AGENTS.md</code> and{" "}
35+ <code className="font-mono text-xs">CLAUDE.md</code> from {branch}: the ones at the root, and the nearest
36+ ones above the files its task touches. Reviews also read{" "}
37+ <code className="font-mono text-xs">.g1t/review.md</code>. Each file is cut at{" "}
38+ {limits.fileChars.toLocaleString()} characters, and all of them at {limits.totalChars.toLocaleString()}.
39+ </p>
40+ <p className="mt-2 flex max-w-2xl items-start gap-2 text-xs text-faint">
41+ <ShieldCheck size={13} className="mt-px shrink-0" />
42+ Only {branch} and the repository's own branches are followed. A pull request from a fork that changes these
43+ files is reviewed as a change, never followed as instructions.
44+ </p>
45+ {files.length === 0 ? (
46+ <div className="mt-3">
47+ <Idle>
48+ No instructions yet. Commit an AGENTS.md to {branch} with how to build, test and work in this repository,
49+ and every agent run reads it.
50+ </Idle>
51+ </div>
52+ ) : (
53+ <ul className="mt-3 space-y-3">
54+ {files.map((file) => (
55+ <li key={file.path} className="rounded-xl border border-line bg-surface">
56+ <details>
57+ <summary className="flex cursor-pointer flex-wrap items-center gap-x-3 gap-y-1 px-4 py-3 text-sm">
58+ <FileText size={14} className="text-muted" />
59+ <span className="font-mono font-medium">{file.path}</span>
60+ <span className="rounded-full bg-raised px-2 py-px text-xs text-muted">{ROLE[file.role]}</span>
61+ {file.truncated && <span className="text-xs text-warn">Longer than agents are given</span>}
62+ <span className="ml-auto flex items-center gap-2 text-xs text-faint">
63+ {file.lastChanged && (
64+ <>
65+ <Link
66+ to={`${base}/commit/${file.lastChanged.commit}`}
67+ className="font-mono hover:text-fg"
68+ title={file.lastChanged.message}
69+ >
70+ {file.lastChanged.commit.slice(0, 7)}
71+ </Link>
72+ <span>by {file.lastChanged.author}</span>
73+ <TimeAgo at={file.lastChanged.at} />
74+ </>
75+ )}
76+ <Link to={`${base}/blob/${encodeURIComponent(branch)}/${file.path}`} className="text-muted hover:text-fg">
77+ Open in code
78+ </Link>
79+ </span>
80+ </summary>
81+ <pre className="max-h-96 overflow-auto border-t border-line px-4 py-3 font-mono text-xs whitespace-pre-wrap text-muted">
82+ {file.text || "(empty)"}
83+ </pre>
84+ </details>
85+ </li>
86+ ))}
87+ </ul>
88+ )}
89+ <p className="mt-3 text-xs text-faint">
90+ To change them, edit the files in the repository and merge the change into {branch}. Agents read the new version
91+ from their next run.
92+ </p>
93+ </section>
94+ );
95+}
+7−0
1919 takesMessages,
2020 } from "@g1t/contracts";
2121
22+import { RunAudit } from "./audit";
2223 import { STAGE_LABEL, StageDots } from "./lifecycle";
2324 import { Avatar, TimeAgo } from "./ui";
2425 import {
361362 <Avatar name={current.agent} size={24} />
362363 <p className="text-sm">
363364 <span className="font-medium">{current.agent}</span>{" "}
365+ {current.startedBy && current.startedBy !== current.agent && (
366+ <span className="text-muted">
367+ on behalf of <span className="text-fg">{current.startedBy}</span>{" "}
368+ </span>
369+ )}
364370 <span className="text-muted">
365371 {active
366372 ? `is ${RUN_KIND_LABEL[current.kind].toLowerCase()}`
406412 {member && active && <MessageRun run={current} />}
407413 {member && active && <StopRun run={current} />}
408414 </div>
415+ {member && <RunAudit owner={owner} repo={repo} runIds={runs.map((run) => run.id)} live={active} />}
409416 </section>
410417 );
411418 }
+181−0
1+/**
2+ * The audit log's entries, as the workspace's Audit log page, an agent
3+ * run's page and a pull request's Agent panel show them.
4+ */
5+
6+import { ShieldAlert, ShieldCheck } from "lucide-react";
7+import { useEffect } from "react";
8+import { Link, useFetcher } from "react-router";
9+
10+import type { AuditEntry } from "@g1t/contracts";
11+
12+import { actionLabel, actorLabel, ruleLabel, targetLabel } from "../lib/audit";
13+import { Avatar, TimeAgo } from "./ui";
14+
15+function clock(at: string): string {
16+ return new Date(at).toISOString().slice(11, 19);
17+}
18+
19+/** Allowed or denied, and by which rule. */
20+export function OutcomeMark({ entry }: { entry: AuditEntry }) {
21+ const denied = entry.outcome === "denied";
22+ return (
23+ <span
24+ title={`${entry.outcome}: ${ruleLabel(entry.rule)} (${entry.rule})`}
25+ className={`inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-xs ${
26+ denied ? "bg-danger/10 text-danger ring-1 ring-danger/30" : "bg-raised text-muted ring-1 ring-line"
27+ }`}
28+ >
29+ {denied ? <ShieldAlert size={11} /> : <ShieldCheck size={11} />}
30+ {entry.outcome}
31+ </span>
32+ );
33+}
34+
35+/** Who acted, with an agent shown as working for someone. */
36+export function ActorLine({ entry }: { entry: AuditEntry }) {
37+ return (
38+ <span className="flex min-w-0 items-center gap-2">
39+ <Avatar name={entry.agent ?? entry.actor} size={18} square={entry.actorKind === "workspace"} />
40+ <span className="truncate">
41+ {entry.onBehalfOf ? (
42+ <>
43+ <span className="font-medium">{entry.agent ?? entry.actor}</span>
44+ <span className="text-muted"> on behalf of </span>
45+ <span className="font-medium">{entry.onBehalfOf}</span>
46+ </>
47+ ) : (
48+ <span className="font-medium">{entry.actor}</span>
49+ )}
50+ </span>
51+ </span>
52+ );
53+}
54+
55+/** The workspace's log, one row an entry, newest first. */
56+export function AuditTable({ entries, base }: { entries: AuditEntry[]; base: string }) {
57+ return (
58+ <ol className="divide-y divide-line rounded-xl border border-line bg-surface">
59+ {entries.map((entry) => (
60+ <li key={entry.id} className="grid gap-x-4 gap-y-1 px-4 py-3 text-sm sm:grid-cols-[9rem_1fr_auto]">
61+ <span className="text-xs leading-5 text-faint">
62+ <TimeAgo at={entry.time} />
63+ </span>
64+ <div className="min-w-0">
65+ <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
66+ <ActorLine entry={entry} />
67+ <span className="font-mono text-xs text-fg/85">{actionLabel(entry.action)}</span>
68+ <span className="truncate font-mono text-xs text-muted">{targetLabel(entry)}</span>
69+ </div>
70+ <p className="mt-1 flex flex-wrap gap-x-3 text-xs text-faint">
71+ <span title={entry.rule}>{ruleLabel(entry.rule)}</span>
72+ <span>{entry.surface.toUpperCase()}</span>
73+ {entry.result && entry.result !== "ok" && <span>result: {entry.result}</span>}
74+ {entry.runId && entry.repo && (
75+ <Link to={`/${entry.repo}/agents/runs/${entry.runId}`} className="hover:text-fg">
76+ {entry.runKind ?? "agent"} run
77+ </Link>
78+ )}
79+ {entry.runId && (
80+ <Link to={`${base}?run=${encodeURIComponent(entry.runId)}`} className="hover:text-fg">
81+ everything this run did
82+ </Link>
83+ )}
84+ {entry.credentialId && (
85+ <span className="font-mono" title="Credential">
86+ {entry.credentialId}
87+ </span>
88+ )}
89+ <span className="font-mono" title="Request id">
90+ {entry.requestId}
91+ </span>
92+ </p>
93+ {entry.outcome === "denied" && entry.message && <p className="mt-1 text-xs text-danger">{entry.message}</p>}
94+ </div>
95+ <span className="sm:text-right">
96+ <OutcomeMark entry={entry} />
97+ </span>
98+ </li>
99+ ))}
100+ </ol>
101+ );
102+}
103+
104+/**
105+ * What an agent's run did, oldest first: every call it made and every git
106+ * request, allowed or refused. `entries` come from the run's audit log.
107+ */
108+export function WhatItDid({ entries, compact = false }: { entries: AuditEntry[]; compact?: boolean }) {
109+ if (entries.length === 0) {
110+ return (
111+ <p className="mt-3 text-sm text-muted">
112+ Nothing recorded yet. Every call this run makes with its credentials, and every clone and push, is listed here.
113+ </p>
114+ );
115+ }
116+ const who = entries.find((entry) => entry.onBehalfOf);
117+ const denied = entries.filter((entry) => entry.outcome === "denied").length;
118+ const shown = compact ? entries.slice(-8) : entries;
119+ return (
120+ <div className="mt-3">
121+ <p className="text-xs text-muted">
122+ {who ? actorLabel(who) : entries[0].actor} · {entries.length} {entries.length === 1 ? "action" : "actions"}
123+ {denied > 0 && <span className="text-danger"> · {denied} refused</span>}
124+ {compact && entries.length > shown.length && ` · the latest ${shown.length}`}
125+ </p>
126+ <ol className="mt-2 divide-y divide-line rounded-xl border border-line bg-surface">
127+ {shown.map((entry) => (
128+ <li key={entry.id} className="flex items-start gap-3 px-4 py-2 text-sm">
129+ <time dateTime={entry.time} className="shrink-0 font-mono text-xs leading-5 text-faint" suppressHydrationWarning>
130+ {clock(entry.time)}
131+ </time>
132+ <div className="min-w-0 grow">
133+ <p className="flex min-w-0 flex-wrap items-center gap-x-2 font-mono text-xs leading-5">
134+ <span className="text-fg/85">{actionLabel(entry.action)}</span>
135+ <span className="truncate text-muted">{targetLabel(entry)}</span>
136+ </p>
137+ {entry.outcome === "denied" && (
138+ <p className="text-xs text-danger">
139+ {entry.message ?? "Refused."} <span className="text-faint">({ruleLabel(entry.rule)})</span>
140+ </p>
141+ )}
142+ </div>
143+ <OutcomeMark entry={entry} />
144+ </li>
145+ ))}
146+ </ol>
147+ </div>
148+ );
149+}
150+
151+/**
152+ * What the runs on a pull request did, fetched from the project's
153+ * `audit.json`, for the Agent panel. Shown to members only.
154+ */
155+export function RunAudit({ owner, repo, runIds, live }: { owner: string; repo: string; runIds: string[]; live: boolean }) {
156+ const fetcher = useFetcher<{ entries: AuditEntry[] }>();
157+ const search = new URLSearchParams(runIds.map((id) => ["run", id])).toString();
158+ const url = `/${owner}/${repo}/audit.json?${search}`;
159+ const { load } = fetcher;
160+ useEffect(() => {
161+ if (runIds.length > 0) load(url);
162+ }, [load, url, runIds.length]);
163+ useEffect(() => {
164+ if (!live) return;
165+ const timer = setInterval(() => {
166+ if (document.visibilityState === "visible") load(url);
167+ }, 8000);
168+ return () => clearInterval(timer);
169+ }, [live, load, url]);
170+ const entries = fetcher.data?.entries;
171+ if (!entries || entries.length === 0) return null;
172+ return (
173+ <details className="mt-3 group">
174+ <summary className="cursor-pointer text-xs text-muted hover:text-fg">What it did</summary>
175+ <WhatItDid entries={entries} compact />
176+ <Link to={`/${owner}/-/audit?project=${encodeURIComponent(repo)}&kind=agent`} className="mt-2 inline-block text-xs text-muted hover:text-fg">
177+ Open the audit log
178+ </Link>
179+ </details>
180+ );
181+}
+566−0
1+/**
2+ * The context hub, as the workspace's Context page and a project's Memory
3+ * page show it: the catalog and its relations, one search over everything,
4+ * scorecards whose failing rules become issues for an agent, and the
5+ * Review queue of memory candidates (keep, edit, dismiss). Pages post to
6+ * their own action with the intents in `reviewAction` and `contextAction`.
7+ */
8+import {
9+ Bot,
10+ Boxes,
11+ Check,
12+ CircleDashed,
13+ FileText,
14+ Globe,
15+ Languages,
16+ Package,
17+ Pencil,
18+ Plug,
19+ Search,
20+ Server,
21+ Sparkles,
22+ User,
23+ Webhook,
24+ X,
25+ XCircle,
26+} from "lucide-react";
27+import { type ReactNode, useEffect, useState } from "react";
28+import { Form, Link, useFetcher, useNavigation } from "react-router";
29+
30+import {
31+ ENTITY_KINDS,
32+ MEMORY_KINDS,
33+ type Catalog,
34+ type Entity,
35+ type EntityKind,
36+ type Memory,
37+ type MemoryKind,
38+ type MemoryReviewApi,
39+ type Result,
40+ type RuleResult,
41+ type Scorecard,
42+ type SearchHit,
43+ type SearchResult,
44+ type User as Actor,
45+} from "@g1t/contracts";
46+
47+import { TimeAgo } from "./ui";
48+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
49+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
50+
51+type Done = { ok: boolean; error?: string; message?: string } | undefined;
52+
53+export const KIND_LABEL: Record<EntityKind, string> = {
54+ project: "Projects",
55+ app: "Apps",
56+ api: "APIs",
57+ package: "Packages",
58+ language: "Languages",
59+ owner: "Owners",
60+ environment: "Environments",
61+ integration: "Integrations",
62+ doc: "Docs",
63+};
64+
65+const KIND_ICON: Record<string, ReactNode> = {
66+ project: <Boxes size={14} />,
67+ app: <Server size={14} />,
68+ api: <Webhook size={14} />,
69+ package: <Package size={14} />,
70+ language: <Languages size={14} />,
71+ owner: <User size={14} />,
72+ environment: <Globe size={14} />,
73+ integration: <Plug size={14} />,
74+ doc: <FileText size={14} />,
75+ memory: <Sparkles size={14} />,
76+ issue: <CircleDashed size={14} />,
77+ pull: <Bot size={14} />,
78+};
79+
80+const TEXTAREA =
81+ "w-full rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim";
82+
83+// --- Review queue -------------------------------------------------------------
84+
85+/** Where a candidate came from, in words, linked where it can be. */
86+function CandidateSource({ memory }: { memory: Memory }) {
87+ const { source } = memory;
88+ const repo = source.repo;
89+ const on = repo && source.number != null ? (
90+ <Link to={`/${repo.namespace}/${repo.name}/pull/${source.number}`} className="hover:text-fg">
91+ {repo.name}#{source.number}
92+ </Link>
93+ ) : null;
94+ const path = source.reference?.startsWith("doc:") ? source.reference.split(":").slice(2).join(":") : null;
95+ const label =
96+ source.kind === "doc" ? (
97+ <>from {repo ? <Link to={`/${repo.namespace}/${repo.name}/blob/HEAD/${path}`} className="hover:text-fg">{path}</Link> : path}</>
98+ ) : source.kind === "review" ? (
99+ <>from a review on {on}</>
100+ ) : source.kind === "pr" ? (
101+ <>from merging {on}</>
102+ ) : source.kind === "run" ? (
103+ <>
104+ learned by{" "}
105+ {source.runId && repo ? (
106+ <Link to={`/${repo.namespace}/${repo.name}/agents/runs/${source.runId}`} className="hover:text-fg">
107+ an agent run
108+ </Link>
109+ ) : (
110+ "an agent run"
111+ )}
112+ {on && <> on {on}</>}
113+ </>
114+ ) : (
115+ <>added by {memory.createdBy}</>
116+ );
117+ return <span>{label}</span>;
118+}
119+
120+function KindSelect({ value }: { value: MemoryKind }) {
121+ return (
122+ <Select name="kind" defaultValue={value}>
123+ <SelectTrigger size="sm" className="w-36">
124+ <SelectValue />
125+ </SelectTrigger>
126+ <SelectContent>
127+ {MEMORY_KINDS.map((kind) => (
128+ <SelectItem key={kind} value={kind}>
129+ {kind[0].toUpperCase() + kind.slice(1)}
130+ </SelectItem>
131+ ))}
132+ </SelectContent>
133+ </Select>
134+ );
135+}
136+
137+function EditAndKeep({ memory, action }: { memory: Memory; action: string }) {
138+ const fetcher = useFetcher<Done>();
139+ const [open, setOpen] = useState(false);
140+ useEffect(() => {
141+ if (fetcher.state === "idle" && fetcher.data?.ok) setOpen(false);
142+ }, [fetcher.state, fetcher.data]);
143+ return (
144+ <Dialog open={open} onOpenChange={setOpen}>
145+ <DialogTrigger className="inline-flex items-center gap-1 rounded-md border border-line px-2 py-1 text-xs text-muted hover:border-line-strong hover:text-fg">
146+ <Pencil size={12} />
147+ Edit
148+ </DialogTrigger>
149+ <DialogContent>
150+ <DialogHeader>
151+ <DialogTitle>Edit and keep</DialogTitle>
152+ <DialogDescription>Agents are given the kept wording from their next run on.</DialogDescription>
153+ </DialogHeader>
154+ <fetcher.Form method="post" action={action} className="space-y-3">
155+ <input type="hidden" name="intent" value="keep" />
156+ <input type="hidden" name="id" value={memory.id} />
157+ <textarea name="text" required rows={4} maxLength={1000} defaultValue={memory.text} className={TEXTAREA} />
158+ <div className="flex items-center justify-between gap-3">
159+ <KindSelect value={memory.kind} />
160+ <button type="submit" disabled={fetcher.state !== "idle"} className="rounded-md bg-fg px-3.5 py-2 text-sm font-medium text-bg hover:bg-white disabled:opacity-50">
161+ Keep
162+ </button>
163+ </div>
164+ {fetcher.data?.error && <p className="text-sm text-danger">{fetcher.data.error}</p>}
165+ </fetcher.Form>
166+ </DialogContent>
167+ </Dialog>
168+ );
169+}
170+
171+function Candidate({ memory, action }: { memory: Memory; action: string }) {
172+ const fetcher = useFetcher<Done>();
173+ const decided = fetcher.formData?.get("intent");
174+ if (decided === "keep" || decided === "dismiss") return null;
175+ return (
176+ <li className="flex gap-3 px-4 py-3">
177+ <div className="min-w-0 grow">
178+ <div className="flex flex-wrap items-center gap-2 text-[0.6875rem] text-muted">
179+ <span className="rounded-full border border-line px-2 py-px">{memory.kind}</span>
180+ <span>{memory.scope === "workspace" ? "every project" : memory.repo ? memory.repo.name : "this project"}</span>
181+ {(memory.seen ?? 1) > 1 && <span>seen {memory.seen} times</span>}
182+ {memory.confidence != null && <span>{Math.round(memory.confidence * 100)}% sure</span>}
183+ </div>
184+ <p className="mt-1.5 text-sm whitespace-pre-wrap">{memory.text}</p>
185+ {memory.source.evidence && (
186+ <p className="mt-1 border-l-2 border-line pl-2 text-xs text-muted whitespace-pre-wrap">{memory.source.evidence}</p>
187+ )}
188+ <p className="mt-1.5 flex flex-wrap gap-x-3 text-xs text-faint">
189+ <CandidateSource memory={memory} />
190+ <span>
191+ <TimeAgo at={memory.updatedAt} />
192+ </span>
193+ </p>
194+ {fetcher.data?.error && <p className="mt-1.5 text-xs text-danger">{fetcher.data.error}</p>}
195+ </div>
196+ <div className="flex shrink-0 items-start gap-1.5">
197+ <button
198+ type="button"
199+ onClick={() => fetcher.submit({ intent: "keep", id: memory.id }, { method: "post", action })}
200+ className="inline-flex items-center gap-1 rounded-md bg-fg px-2 py-1 text-xs font-medium text-bg hover:bg-white"
201+ >
202+ <Check size={12} />
203+ Keep
204+ </button>
205+ <EditAndKeep memory={memory} action={action} />
206+ <button
207+ type="button"
208+ aria-label="Dismiss"
209+ title="Dismiss: never suggested again in these words"
210+ onClick={() => fetcher.submit({ intent: "dismiss", id: memory.id }, { method: "post", action })}
211+ className="inline-flex items-center gap-1 rounded-md border border-line px-2 py-1 text-xs text-muted hover:border-line-strong hover:text-danger"
212+ >
213+ <X size={12} />
214+ Dismiss
215+ </button>
216+ </div>
217+ </li>
218+ );
219+}
220+
221+/** Memory candidates waiting for a person: keep, edit and keep, or dismiss. */
222+export function ReviewQueue({ candidates, action, empty }: { candidates: Memory[]; action: string; empty: string }) {
223+ if (candidates.length === 0) {
224+ return <p className="rounded-xl border border-dashed border-line px-4 py-6 text-sm text-muted">{empty}</p>;
225+ }
226+ return (
227+ <ul className="divide-y divide-line rounded-xl border border-line bg-surface">
228+ {candidates.map((memory) => (
229+ <Candidate key={memory.id} memory={memory} action={action} />
230+ ))}
231+ </ul>
232+ );
233+}
234+
235+/** The intents of the Review queue: keep (with an edit, or as it is) and dismiss. Null for other intents. */
236+export async function reviewAction(api: MemoryReviewApi, actor: Actor, workspace: string, form: FormData): Promise<Done | null> {
237+ const intent = String(form.get("intent") ?? "");
238+ if (intent !== "keep" && intent !== "dismiss") return null;
239+ const text = form.get("text");
240+ const kind = String(form.get("kind") ?? "");
241+ const reviewed = await api.reviewMemory(actor, workspace, String(form.get("id") ?? ""), intent, {
242+ text: text == null ? undefined : String(text),
243+ kind: MEMORY_KINDS.includes(kind as MemoryKind) ? (kind as MemoryKind) : undefined,
244+ });
245+ return reviewed.ok ? { ok: true } : { ok: false, error: reviewed.error.message };
246+}
247+
248+// --- Catalog ------------------------------------------------------------------
249+
250+/**
251+ * The projects and how they depend on each other, drawn in a circle; a
252+ * project's owners and apps hang off it. Small workspaces only: past 24
253+ * projects the list says it all.
254+ */
255+export function RelationsGraph({ catalog }: { catalog: Catalog }) {
256+ const projects = catalog.entities.filter((entity) => entity.kind === "project").slice(0, 24);
257+ if (projects.length < 2) return null;
258+ const size = 420;
259+ const center = size / 2;
260+ const radius = size / 2 - 60;
261+ const at = new Map(
262+ projects.map((project, i) => {
263+ const angle = (2 * Math.PI * i) / projects.length - Math.PI / 2;
264+ return [project.id, { x: center + radius * Math.cos(angle), y: center + radius * Math.sin(angle), project }];
265+ }),
266+ );
267+ const edges = catalog.relations.filter((relation) => relation.kind === "depends_on" && at.has(relation.from) && at.has(relation.to));
268+ return (
269+ <figure className="rounded-xl border border-line bg-surface p-3">
270+ <svg viewBox={`0 0 ${size} ${size}`} className="mx-auto block h-auto w-full max-w-md" role="img" aria-label="How the workspace's projects depend on each other">
271+ <defs>
272+ <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
273+ <path d="M 0 0 L 10 5 L 0 10 z" className="fill-muted" />
274+ </marker>
275+ </defs>
276+ {edges.map((edge) => {
277+ const from = at.get(edge.from)!;
278+ const to = at.get(edge.to)!;
279+ const dx = to.x - from.x;
280+ const dy = to.y - from.y;
281+ const length = Math.hypot(dx, dy) || 1;
282+ const pad = 26;
283+ return (
284+ <line
285+ key={`${edge.from}-${edge.to}`}
286+ x1={from.x + (dx / length) * pad}
287+ y1={from.y + (dy / length) * pad}
288+ x2={to.x - (dx / length) * pad}
289+ y2={to.y - (dy / length) * pad}
290+ className="stroke-muted"
291+ strokeWidth={1.25}
292+ markerEnd="url(#arrow)"
293+ />
294+ );
295+ })}
296+ {[...at.values()].map(({ x, y, project }) => (
297+ <g key={project.id}>
298+ <circle cx={x} cy={y} r={20} className="fill-bg stroke-accent" strokeWidth={1.5} />
299+ <text x={x} y={y + 36} textAnchor="middle" className="fill-fg text-[11px]">
300+ {project.name.length > 16 ? `${project.name.slice(0, 15)}…` : project.name}
301+ </text>
302+ <text x={x} y={y + 4} textAnchor="middle" className="fill-accent text-[11px] font-semibold">
303+ {project.name[0]?.toUpperCase()}
304+ </text>
305+ </g>
306+ ))}
307+ </svg>
308+ <figcaption className="mt-1 text-center text-xs text-faint">An arrow points from a project to one it uses.</figcaption>
309+ </figure>
310+ );
311+}
312+
313+function EntityRow({ entity, names }: { entity: Entity; names: Map<string, Entity> }) {
314+ const link = entity.ref?.startsWith("/") ? (
315+ <Link to={entity.ref} className="font-medium hover:text-accent">
316+ {entity.name}
317+ </Link>
318+ ) : entity.ref ? (
319+ <a href={entity.ref} className="font-medium hover:text-accent" rel="noreferrer">
320+ {entity.name}
321+ </a>
322+ ) : (
323+ <span className="font-medium">{entity.name}</span>
324+ );
325+ void names;
326+ return (
327+ <li className="flex gap-3 px-4 py-2.5">
328+ <span className="mt-0.5 text-muted">{KIND_ICON[entity.kind]}</span>
329+ <div className="min-w-0 grow">
330+ <div className="flex flex-wrap items-baseline gap-x-2 text-sm">
331+ {link}
332+ {entity.project && entity.kind !== "project" && <span className="text-xs text-faint">{entity.project}</span>}
333+ {entity.private && <span className="text-[0.6875rem] text-faint">private</span>}
334+ </div>
335+ {entity.summary && <p className="mt-0.5 text-xs text-muted">{entity.summary}</p>}
336+ </div>
337+ </li>
338+ );
339+}
340+
341+/** The catalog: filters by kind and project, the graph, and the entries. */
342+export function CatalogView({ catalog, kind, project, base }: { catalog: Catalog; kind: EntityKind | null; project: string | null; base: string }) {
343+ const projects = catalog.entities.filter((entity) => entity.kind === "project");
344+ const names = new Map(catalog.entities.map((entity) => [entity.id, entity]));
345+ const shown = catalog.entities.filter((entity) => (!kind || entity.kind === kind) && (!project || entity.project === project || entity.project == null));
346+ const counts = new Map<string, number>();
347+ for (const entity of catalog.entities) counts.set(entity.kind, (counts.get(entity.kind) ?? 0) + 1);
348+ const href = (k: string | null, p: string | null) => {
349+ const params = new URLSearchParams({ tab: "catalog" });
350+ if (k) params.set("kind", k);
351+ if (p) params.set("project", p);
352+ return `${base}?${params}`;
353+ };
354+ return (
355+ <div className="space-y-5">
356+ <div className="flex flex-wrap gap-1.5">
357+ <Link to={href(null, project)} className={`rounded-full border px-2.5 py-1 text-xs ${!kind ? "border-accent text-fg" : "border-line text-muted hover:text-fg"}`}>
358+ Everything {catalog.entities.length}
359+ </Link>
360+ {ENTITY_KINDS.filter((k) => counts.get(k)).map((k) => (
361+ <Link key={k} to={href(k, project)} className={`inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-xs ${kind === k ? "border-accent text-fg" : "border-line text-muted hover:text-fg"}`}>
362+ {KIND_ICON[k]}
363+ {KIND_LABEL[k]} {counts.get(k)}
364+ </Link>
365+ ))}
366+ </div>
367+ {projects.length > 1 && (
368+ <div className="flex flex-wrap items-center gap-1.5 text-xs">
369+ <span className="text-faint">Project:</span>
370+ <Link to={href(kind, null)} className={!project ? "text-fg" : "text-muted hover:text-fg"}>
371+ all
372+ </Link>
373+ {projects.map((p) => (
374+ <Link key={p.id} to={href(kind, p.key)} className={project === p.key ? "text-fg" : "text-muted hover:text-fg"}>
375+ {p.name}
376+ </Link>
377+ ))}
378+ </div>
379+ )}
380+ {!kind && !project && <RelationsGraph catalog={catalog} />}
381+ {shown.length === 0 ? (
382+ <p className="rounded-xl border border-dashed border-line px-4 py-6 text-sm text-muted">
383+ Nothing here yet. The catalog builds itself from each project's default branch: its manifests, docs and
384+ workflows, with its deployments and integrations.
385+ </p>
386+ ) : (
387+ <ul className="divide-y divide-line rounded-xl border border-line bg-surface">
388+ {shown.slice(0, 500).map((entity) => (
389+ <EntityRow key={entity.id} entity={entity} names={names} />
390+ ))}
391+ </ul>
392+ )}
393+ {catalog.builtAt && (
394+ <p className="text-xs text-faint">
395+ Last built <TimeAgo at={catalog.builtAt} />.
396+ </p>
397+ )}
398+ </div>
399+ );
400+}
401+
402+// --- Search -------------------------------------------------------------------
403+
404+function Hit({ hit }: { hit: SearchHit }) {
405+ const title = hit.url?.startsWith("/") ? (
406+ <Link to={hit.url} className="font-medium hover:text-accent">
407+ {hit.title}
408+ </Link>
409+ ) : hit.url ? (
410+ <a href={hit.url} className="font-medium hover:text-accent" rel="noreferrer">
411+ {hit.title}
412+ </a>
413+ ) : (
414+ <span className="font-medium">{hit.title}</span>
415+ );
416+ return (
417+ <li className="flex gap-3 px-4 py-3">
418+ <span className="mt-0.5 text-muted">{KIND_ICON[hit.kind]}</span>
419+ <div className="min-w-0 grow">
420+ <div className="text-sm">{title}</div>
421+ {hit.snippet && <p className="mt-0.5 text-xs text-muted">{hit.snippet}</p>}
422+ <p className="mt-1 flex flex-wrap gap-x-3 text-[0.6875rem] text-faint">
423+ <span>{hit.kind}</span>
424+ <span>{hit.source}</span>
425+ {hit.project && <span>{hit.project}</span>}
426+ {hit.by && <span>by {hit.by}</span>}
427+ {hit.updatedAt && (
428+ <span>
429+ <TimeAgo at={hit.updatedAt} />
430+ </span>
431+ )}
432+ </p>
433+ </div>
434+ </li>
435+ );
436+}
437+
438+/** One search over the catalog, docs, issues, pull requests and memory. */
439+export function SearchView({ result, query, base }: { result: SearchResult | null; query: string; base: string }) {
440+ const navigation = useNavigation();
441+ const searching = navigation.state === "loading" && navigation.location?.search.includes("tab=search");
442+ return (
443+ <div className="space-y-4">
444+ <Form method="get" action={base} className="flex gap-2">
445+ <input type="hidden" name="tab" value="search" />
446+ <label className="relative grow">
447+ <Search size={15} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
448+ <input
449+ name="q"
450+ defaultValue={query}
451+ placeholder="How do we deploy the api? Who owns billing? Why did we keep v1 webhooks?"
452+ autoComplete="off"
453+ data-1p-ignore
454+ className="w-full rounded-md border border-line bg-bg py-2 pr-3 pl-9 text-sm outline-none placeholder:text-faint hover:border-line-strong focus:border-accent-dim"
455+ />
456+ </label>
457+ <button type="submit" className="rounded-md bg-fg px-3.5 py-2 text-sm font-medium text-bg hover:bg-white">
458+ {searching ? "Searching…" : "Search"}
459+ </button>
460+ </Form>
461+ {result &&
462+ (result.hits.length === 0 ? (
463+ <p className="rounded-xl border border-dashed border-line px-4 py-6 text-sm text-muted">Nothing matched “{result.query}”.</p>
464+ ) : (
465+ <>
466+ <ul className="divide-y divide-line rounded-xl border border-line bg-surface">
467+ {result.hits.map((hit) => (
468+ <Hit key={`${hit.kind}:${hit.id}`} hit={hit} />
469+ ))}
470+ </ul>
471+ <p className="text-xs text-faint">
472+ {result.mode === "semantic" ? "Ranked by meaning, then by matching words." : "Matched by words: the search index did not answer."}
473+ </p>
474+ </>
475+ ))}
476+ </div>
477+ );
478+}
479+
480+// --- Scorecards ---------------------------------------------------------------
481+
482+function Rule({ rule, project, action }: { rule: RuleResult; project: string; action: string }) {
483+ const fetcher = useFetcher<Done>();
484+ const icon =
485+ rule.status === "pass" ? <Check size={14} className="text-accent" /> : rule.status === "fail" ? <XCircle size={14} className="text-danger" /> : <CircleDashed size={14} className="text-faint" />;
486+ return (
487+ <li className="flex items-start gap-2.5 py-1.5">
488+ <span className="mt-0.5">{icon}</span>
489+ <div className="min-w-0 grow text-sm">
490+ <span className={rule.status === "na" ? "text-muted" : ""}>{rule.title}</span>
491+ <p className="text-xs text-muted">{rule.detail}</p>
492+ {fetcher.data?.message && <p className="mt-1 text-xs text-accent">{fetcher.data.message}</p>}
493+ {fetcher.data?.error && <p className="mt-1 text-xs text-danger">{fetcher.data.error}</p>}
494+ </div>
495+ {rule.fix && !fetcher.data?.ok && (
496+ <fetcher.Form method="post" action={action}>
497+ <input type="hidden" name="intent" value="fix" />
498+ <input type="hidden" name="project" value={project} />
499+ <input type="hidden" name="rule" value={rule.rule} />
500+ <button
501+ type="submit"
502+ disabled={fetcher.state !== "idle"}
503+ title={`Opens “${rule.fix.title}” and puts an agent on it`}
504+ className="inline-flex shrink-0 items-center gap-1 rounded-md border border-line px-2 py-1 text-xs text-muted hover:border-accent-dim hover:text-fg disabled:opacity-50"
505+ >
506+ <Bot size={12} />
507+ {fetcher.state !== "idle" ? "Opening…" : "Fix with an agent"}
508+ </button>
509+ </fetcher.Form>
510+ )}
511+ </li>
512+ );
513+}
514+
515+/** Each project's scorecard; a failing rule becomes an issue for an agent in one click. */
516+export function ScorecardsView({ cards, action }: { cards: Scorecard[]; action: string }) {
517+ if (cards.length === 0) return <p className="rounded-xl border border-dashed border-line px-4 py-6 text-sm text-muted">No projects yet.</p>;
518+ return (
519+ <div className="grid gap-3 md:grid-cols-2">
520+ {cards.map((card) => (
521+ <section key={card.project} className="rounded-xl border border-line bg-surface p-4">
522+ <div className="flex items-baseline justify-between gap-3">
523+ <Link to={`/${card.repo.namespace}/${card.repo.name}`} className="font-medium hover:text-accent">
524+ {card.name}
525+ </Link>
526+ <span className={`text-xs ${card.passed === card.total ? "text-accent" : "text-muted"}`}>
527+ {card.passed} of {card.total}
528+ </span>
529+ </div>
530+ <ul className="mt-2">
531+ {card.rules.map((rule) => (
532+ <Rule key={rule.rule} rule={rule} project={card.project} action={action} />
533+ ))}
534+ </ul>
535+ </section>
536+ ))}
537+ </div>
538+ );
539+}
540+
541+export type ContextActionResult = Done;
542+
543+/** Opens a scorecard rule's fix as an issue and puts g1t's agent on it. */
544+export async function fixRule(
545+ deps: {
546+ scorecards: () => Promise<Result<Scorecard[]>>;
547+ openIssue: (repo: { namespace: string; name: string }, input: { title: string; body: string; checks: string[] }) => Promise<Result<{ number: number }>>;
548+ assign: (repo: { namespace: string; name: string }, number: number) => Promise<unknown>;
549+ },
550+ project: string,
551+ rule: string,
552+): Promise<Done> {
553+ const cards = await deps.scorecards();
554+ if (!cards.ok) return { ok: false, error: cards.error.message };
555+ const card = cards.value.find((c) => c.project === project);
556+ const fix = card?.rules.find((r) => r.rule === rule && r.status === "fail")?.fix;
557+ if (!card || !fix) return { ok: false, error: "That rule passes now, or no longer applies." };
558+ const opened = await deps.openIssue(card.repo, {
559+ title: fix.title,
560+ body: `${fix.body}\n\nOpened from ${card.name}'s scorecard (${rule}).`,
561+ checks: fix.checks,
562+ });
563+ if (!opened.ok) return { ok: false, error: opened.error.message };
564+ await deps.assign(card.repo, opened.value.number);
565+ return { ok: true, message: `Opened #${opened.value.number}; an agent is on it.` };
566+}
+382−0
1+/**
2+ * Guardrails: the form a workspace sets its defaults with and a project its
3+ * overrides, and what a run shows of its caps.
4+ */
5+import { Clock, Coins, ShieldCheck } from "lucide-react";
6+import { type ReactNode, useEffect, useState } from "react";
7+import { Form } from "react-router";
8+
9+import {
10+ type AgentRun,
11+ type GuardrailSettings,
12+ type Guardrails,
13+ type GuardrailsView,
14+ RUN_KINDS,
15+ RUN_KIND_LABEL,
16+ isActiveRun,
17+} from "@g1t/contracts";
18+
19+import { formatCap, tri } from "../lib/guardrails";
20+import { formatCost } from "./agents";
21+import { Button, ErrorText, Input, TimeAgo } from "./ui";
22+import { CheckboxOption } from "./ui/checkbox";
23+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
24+import { Textarea } from "./ui/textarea";
25+
26+function Section({ title, about, children }: { title: string; about: ReactNode; children: ReactNode }) {
27+ return (
28+ <section className="grid gap-x-10 gap-y-4 border-t border-line pt-8 first:border-t-0 first:pt-0 lg:grid-cols-[16rem_1fr]">
29+ <div>
30+ <h2 className="font-medium">{title}</h2>
31+ <div className="mt-1 text-sm text-muted">{about}</div>
32+ </div>
33+ <div className="min-w-0 space-y-3">{children}</div>
34+ </section>
35+ );
36+}
37+
38+/** A choice of inheriting, or on or off, for one setting. */
39+function TriSelect({
40+ name,
41+ value,
42+ inherited,
43+ parent,
44+ labels,
45+ title,
46+ children,
47+}: {
48+ name: string;
49+ value: boolean | null | undefined;
50+ inherited: boolean;
51+ parent: string;
52+ labels: [on: string, off: string];
53+ title: string;
54+ children: ReactNode;
55+}) {
56+ return (
57+ <div className="flex flex-col gap-3 rounded-xl border border-line bg-surface p-4 sm:flex-row sm:items-start">
58+ <div className="min-w-0 grow">
59+ <p className="text-sm font-medium">{title}</p>
60+ <p className="mt-1 text-sm text-muted">{children}</p>
61+ </div>
62+ <Select name={name} defaultValue={tri(value)}>
63+ <SelectTrigger size="sm" aria-label={title} className="w-full shrink-0 sm:w-auto sm:min-w-44">
64+ <SelectValue />
65+ </SelectTrigger>
66+ <SelectContent align="end">
67+ <SelectItem value="inherit">
68+ As {parent} ({(inherited ? labels[0] : labels[1]).toLowerCase()})
69+ </SelectItem>
70+ <SelectItem value="on">{labels[0]}</SelectItem>
71+ <SelectItem value="off">{labels[1]}</SelectItem>
72+ </SelectContent>
73+ </Select>
74+ </div>
75+ );
76+}
77+
78+/** Hosts in a compact list. */
79+function Hosts({ hosts }: { hosts: string[] }) {
80+ return (
81+ <span className="font-mono text-xs break-words text-faint">{hosts.length ? hosts.join(", ") : "none"}</span>
82+ );
83+}
84+
85+/**
86+ * One level's guardrails. `level` says which: the workspace's defaults,
87+ * over g1t's, or a project's overrides, over its workspace's.
88+ */
89+export function GuardrailsForm({
90+ view,
91+ level,
92+ editable,
93+ saving,
94+ saved,
95+ error,
96+}: {
97+ view: GuardrailsView;
98+ level: "workspace" | "project";
99+ editable: boolean;
100+ saving: boolean;
101+ saved: boolean;
102+ error: string | null | undefined;
103+}) {
104+ const own: GuardrailSettings = (level === "project" ? view.project : view.workspace) ?? {};
105+ // What this level inherits, and what it is called.
106+ const base: Guardrails = level === "project" ? view.inherited : view.defaults;
107+ const parent = level === "project" ? "the workspace" : "g1t's default";
108+ const [registryMode, setRegistryMode] = useState(own.registries ? "custom" : "inherit");
109+ const shownRegistries = own.registries ?? base.registries;
110+ const inheritedDomains = level === "project" ? view.inherited.domains : [];
111+ const inheritedDeny = level === "project" ? view.inherited.deny : [];
112+ return (
113+ <Form method="post" className="max-w-4xl space-y-8">
114+ <fieldset disabled={!editable} className="min-w-0 space-y-8">
115+ <Section
116+ title="Network"
117+ about={
118+ <>
119+ Which hosts a sandbox may reach. Requests anywhere else are refused at the sandbox's edge, and
120+ each refused host shows on the run as a step.
121+ </>
122+ }
123+ >
124+ <TriSelect
125+ name="restrictNetwork"
126+ value={own.restrictNetwork}
127+ inherited={base.restrictNetwork}
128+ parent={parent}
129+ labels={["Restricted", "Open"]}
130+ title="Only allowed hosts"
131+ >
132+ Restricted, a sandbox reaches g1t, the registries below and the domains you list, over HTTP and
133+ HTTPS only. Open, it reaches the whole internet.
134+ </TriSelect>
135+ <div className="rounded-xl border border-line bg-surface p-4">
136+ <p className="text-sm font-medium">Always allowed</p>
137+ <p className="mt-1 text-sm text-muted">
138+ g1t's own hosts, for cloning, pushing, reporting and the model: <Hosts hosts={view.g1tHosts} />
139+ </p>
140+ </div>
141+ <div className="rounded-xl border border-line bg-surface p-4">
142+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start">
143+ <div className="min-w-0 grow">
144+ <p className="text-sm font-medium">Package registries</p>
145+ <p className="mt-1 text-sm text-muted">Where installs fetch dependencies from.</p>
146+ </div>
147+ <Select name="registries" value={registryMode} onValueChange={setRegistryMode}>
148+ <SelectTrigger size="sm" aria-label="Package registries" className="w-full shrink-0 sm:w-auto sm:min-w-44">
149+ <SelectValue />
150+ </SelectTrigger>
151+ <SelectContent align="end">
152+ <SelectItem value="inherit">As {parent}</SelectItem>
153+ <SelectItem value="custom">Choose</SelectItem>
154+ </SelectContent>
155+ </Select>
156+ </div>
157+ <div className="mt-4 grid gap-3 sm:grid-cols-2">
158+ {view.registries.map((registry) => (
159+ <CheckboxOption
160+ key={`${registry.id}-${registryMode}`}
161+ name={`registry:${registry.id}`}
162+ disabled={registryMode !== "custom"}
163+ defaultChecked={(registryMode === "custom" ? shownRegistries : base.registries).includes(registry.id)}
164+ label={registry.name}
165+ description={registry.hosts.join(", ")}
166+ />
167+ ))}
168+ </div>
169+ </div>
170+ <div className="rounded-xl border border-line bg-surface p-4">
171+ <label htmlFor="guardrail-domains" className="text-sm font-medium">
172+ Allowed domains
173+ </label>
174+ <p className="mt-1 text-sm text-muted">
175+ One per line: <span className="font-mono text-xs">api.stripe.com</span>, or{" "}
176+ <span className="font-mono text-xs">*.example.com</span> for its subdomains.
177+ {level === "project" && " These add to the workspace's."}
178+ </p>
179+ <Textarea
180+ id="guardrail-domains"
181+ name="domains"
182+ className="mt-3 font-mono text-xs"
183+ rows={4}
184+ defaultValue={(own.domains ?? []).join("\n")}
185+ placeholder="api.stripe.com"
186+ />
187+ {inheritedDomains.length > 0 && (
188+ <p className="mt-2 text-xs text-faint">
189+ From the workspace: <Hosts hosts={inheritedDomains} />
190+ </p>
191+ )}
192+ </div>
193+ </Section>
194+
195+ <Section
196+ title="Commands"
197+ about={
198+ <>
199+ What the agent's harness refuses to run. A refused command is not run; the agent is told why, and
200+ the run shows it as a step.
201+ </>
202+ }
203+ >
204+ {view.rules.map((rule) => (
205+ <TriSelect
206+ key={rule.id}
207+ name={`rule:${rule.id}`}
208+ value={own.rules?.[rule.id]}
209+ inherited={base.rules[rule.id] ?? true}
210+ parent={parent}
211+ labels={["On", "Off"]}
212+ title={rule.title}
213+ >
214+ {rule.about}
215+ </TriSelect>
216+ ))}
217+ <div className="rounded-xl border border-line bg-surface p-4">
218+ <label htmlFor="guardrail-deny" className="text-sm font-medium">
219+ Also refuse
220+ </label>
221+ <p className="mt-1 text-sm text-muted">
222+ One rule per line, as <span className="font-mono text-xs">Bash(terraform apply:*)</span>,{" "}
223+ <span className="font-mono text-xs">Edit(//etc/**)</span> or{" "}
224+ <span className="font-mono text-xs">WebFetch</span>. Plain text is the start of a shell command.
225+ {level === "project" && " These add to the workspace's."}
226+ </p>
227+ <Textarea
228+ id="guardrail-deny"
229+ name="deny"
230+ className="mt-3 font-mono text-xs"
231+ rows={4}
232+ defaultValue={(own.deny ?? []).join("\n")}
233+ placeholder="Bash(terraform apply:*)"
234+ />
235+ {inheritedDeny.length > 0 && (
236+ <p className="mt-2 text-xs text-faint">
237+ From the workspace: <span className="font-mono">{inheritedDeny.join(", ")}</span>
238+ </p>
239+ )}
240+ </div>
241+ </Section>
242+
243+ <Section
244+ title="Caps"
245+ about="How much one run may cost and how long it may take. A run that reaches either is stopped, and its pull request waits for you."
246+ >
247+ <div className="flex flex-col gap-3 rounded-xl border border-line bg-surface p-4 sm:flex-row sm:items-start">
248+ <div className="min-w-0 grow">
249+ <label htmlFor="guardrail-budget" className="text-sm font-medium">
250+ Cost per run, in US dollars
251+ </label>
252+ <p className="mt-1 text-sm text-muted">
253+ Empty: as {parent} ({formatCap(base.budgetUsd)}). 0: no cap.
254+ </p>
255+ </div>
256+ <Input
257+ id="guardrail-budget"
258+ name="budgetUsd"
259+ inputMode="decimal"
260+ className="w-full shrink-0 sm:w-32"
261+ defaultValue={own.budgetUsd == null ? "" : String(own.budgetUsd)}
262+ placeholder={base.budgetUsd == null ? "0" : base.budgetUsd.toFixed(2)}
263+ />
264+ </div>
265+ <div className="rounded-xl border border-line bg-surface p-4">
266+ <p className="text-sm font-medium">Time per run, in minutes</p>
267+ <p className="mt-1 text-sm text-muted">Empty: as {parent}, shown faded.</p>
268+ <div className="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3">
269+ {RUN_KINDS.map((kind) => (
270+ <label key={kind} className="flex flex-col gap-1 text-xs text-muted">
271+ {RUN_KIND_LABEL[kind]}
272+ <Input
273+ name={`minutes:${kind}`}
274+ inputMode="numeric"
275+ defaultValue={own.minutes?.[kind] == null ? "" : String(own.minutes[kind])}
276+ placeholder={String(base.minutes[kind] ?? "")}
277+ />
278+ </label>
279+ ))}
280+ </div>
281+ </div>
282+ </Section>
283+ </fieldset>
284+
285+ {editable ? (
286+ <div className="sticky bottom-0 -mx-4 flex flex-wrap items-center gap-4 border-t border-line bg-bg/90 px-4 py-4 backdrop-blur">
287+ <Button type="submit" disabled={saving}>
288+ {saving ? "Saving…" : "Save guardrails"}
289+ </Button>
290+ {saved && <span className="text-sm text-muted">Saved. Runs that start from now on get these.</span>}
291+ <ErrorText>{error}</ErrorText>
292+ {own.updatedBy && own.updatedAt && !saved && !error && (
293+ <span className="text-xs text-faint">
294+ Last changed by <span className="font-mono">{own.updatedBy}</span> <TimeAgo at={own.updatedAt} />
295+ </span>
296+ )}
297+ </div>
298+ ) : (
299+ <p className="text-sm text-muted">
300+ {level === "workspace"
301+ ? "Only the workspace's owners can change its defaults."
302+ : "Only members of the workspace can change a project's guardrails."}
303+ </p>
304+ )}
305+ </Form>
306+ );
307+}
308+
309+/** Minutes since `from`, ticking while the run goes on. */
310+function useMinutes(from: string | null, to: string | null): number | null {
311+ const [now, setNow] = useState(() => Date.now());
312+ useEffect(() => {
313+ if (!from || to) return;
314+ const timer = setInterval(() => setNow(Date.now()), 5000);
315+ return () => clearInterval(timer);
316+ }, [from, to]);
317+ if (!from) return null;
318+ return ((to ? new Date(to).getTime() : now) - new Date(from).getTime()) / 60000;
319+}
320+
321+function Meter({ share, tone }: { share: number | null; tone: string }) {
322+ if (share == null) return null;
323+ return (
324+ <span className="mt-2 block h-1 overflow-hidden rounded-full bg-line" aria-hidden>
325+ <span className={`block h-full rounded-full ${tone}`} style={{ width: `${Math.round(share * 100)}%` }} />
326+ </span>
327+ );
328+}
329+
330+/** A run's caps: spent and elapsed against them, and whether one stopped it. */
331+export function RunCaps({ run, member }: { run: AgentRun; member: boolean }) {
332+ const active = isActiveRun(run.status);
333+ const minutes = useMinutes(run.startedAt ?? (active ? run.createdAt : null), run.finishedAt);
334+ if (run.timeCapMinutes == null && run.budgetUsd == null && !run.halted) return null;
335+ const cap = run.timeCapMinutes ?? null;
336+ const timeShare = cap && minutes != null ? Math.min(1, minutes / cap) : null;
337+ const spent = formatCost(run.costUsd);
338+ const budget = run.budgetUsd ?? null;
339+ const costShare = budget && run.costUsd != null ? Math.min(1, run.costUsd / budget) : null;
340+ return (
341+ <section className="mt-6 rounded-xl border border-line bg-surface p-4">
342+ <div className="flex items-center gap-2 text-sm font-medium">
343+ <ShieldCheck size={15} className="text-accent" />
344+ Guardrails
345+ {run.halted && (
346+ <span className="ml-auto rounded-full border border-warn/40 bg-warn/10 px-2 py-0.5 text-xs font-normal text-warn">
347+ Stopped at its {run.halted === "budget" ? "cost" : "time"} cap
348+ </span>
349+ )}
350+ </div>
351+ <div className="mt-3 grid gap-4 text-sm sm:grid-cols-2">
352+ {cap != null && (
353+ <div>
354+ <p className="flex items-center gap-1.5 text-muted">
355+ <Clock size={13} />
356+ <span className="tabular-nums text-fg" suppressHydrationWarning>
357+ {minutes == null ? "—" : `${Math.floor(minutes)}m`}
358+ </span>
359+ of {cap}m
360+ </p>
361+ <Meter share={timeShare} tone={timeShare != null && timeShare > 0.85 ? "bg-warn" : "bg-accent"} />
362+ </div>
363+ )}
364+ {member && (
365+ <div>
366+ <p className="flex items-center gap-1.5 text-muted">
367+ <Coins size={13} />
368+ {spent ? <span className="tabular-nums text-fg">{spent}</span> : <span>Spend</span>}
369+ of {formatCap(budget)}
370+ </p>
371+ <Meter share={costShare} tone={costShare != null && costShare > 0.85 ? "bg-warn" : "bg-accent"} />
372+ {active && !spent && budget != null && (
373+ <p className="mt-1.5 text-xs text-faint">
374+ The agent stops itself at the cap. What it spent is reported when the run ends.
375+ </p>
376+ )}
377+ </div>
378+ )}
379+ </div>
380+ </section>
381+ );
382+}
+74−0
1+import { Bot } from "lucide-react";
2+import { type ComponentProps, type KeyboardEvent, useRef, useState } from "react";
3+
4+import { AGENT_MENTION as AGENT_HANDLE, partialMention } from "../lib/mention";
5+import { Textarea } from "./ui";
6+
7+/**
8+ * A comment box that offers to complete `@g1t-agent`: Tab or Enter takes
9+ * the suggestion, Escape dismisses it.
10+ */
11+export function MentionTextarea(props: ComponentProps<"textarea">) {
12+ const ref = useRef<HTMLTextAreaElement>(null);
13+ const [start, setStart] = useState<number | null>(null);
14+
15+ const look = () => {
16+ const box = ref.current;
17+ if (!box) return;
18+ setStart(box.selectionStart === box.selectionEnd ? partialMention(box.value, box.selectionStart) : null);
19+ };
20+
21+ const complete = () => {
22+ const box = ref.current;
23+ if (!box || start == null) return;
24+ const caret = box.selectionStart;
25+ box.setRangeText(`${AGENT_HANDLE} `, start, caret, "end");
26+ setStart(null);
27+ box.focus();
28+ };
29+
30+ const onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
31+ if (start != null && (event.key === "Tab" || event.key === "Enter") && !event.shiftKey) {
32+ event.preventDefault();
33+ complete();
34+ return;
35+ }
36+ if (event.key === "Escape") setStart(null);
37+ props.onKeyDown?.(event);
38+ };
39+
40+ return (
41+ <div className="relative">
42+ <Textarea
43+ {...props}
44+ ref={ref}
45+ onKeyDown={onKeyDown}
46+ onInput={(event) => {
47+ look();
48+ props.onInput?.(event);
49+ }}
50+ onClick={(event) => {
51+ look();
52+ props.onClick?.(event);
53+ }}
54+ onBlur={(event) => {
55+ // After a click on the suggestion has landed.
56+ setTimeout(() => setStart(null), 150);
57+ props.onBlur?.(event);
58+ }}
59+ />
60+ {start != null && (
61+ <button
62+ type="button"
63+ onMouseDown={(event) => event.preventDefault()}
64+ onClick={complete}
65+ className="absolute bottom-2 left-2 flex items-center gap-1.5 rounded-md border border-line bg-raised px-2 py-1 font-mono text-xs text-fg shadow-sm"
66+ >
67+ <Bot size={12} className="text-merged" />
68+ {AGENT_HANDLE}
69+ <span className="font-sans text-faint">Tab</span>
70+ </button>
71+ )}
72+ </div>
73+ );
74+}
+515−0
1+/**
2+ * The pieces mission control and a project's overview are made of: panels
3+ * with a "View all", what needs the viewer, the activity feed with its
4+ * filters, the week's pulse with its sparklines, a project's pipeline and
5+ * its deploy history.
6+ */
7+import {
8+ ArrowRight,
9+ Brain,
10+ CircleCheck,
11+ CircleDot,
12+ CircleSlash,
13+ CreditCard,
14+ Eye,
15+ GitMerge,
16+ GitPullRequest,
17+ Hand,
18+ MessageCircleQuestion,
19+ MessageSquare,
20+ Play,
21+ Rocket,
22+ Swords,
23+ Terminal,
24+ TimerOff,
25+ TriangleAlert,
26+} from "lucide-react";
27+import { type ReactNode, useMemo, useState } from "react";
28+import { Link } from "react-router";
29+
30+import type { DeployStatus } from "@g1t/contracts";
31+
32+import { cn } from "../lib/cn";
33+import { type ActivityGroup, type Need, type NeedKind, type Verb, isAgent, sparkPoints } from "../lib/mission";
34+import { Avatar, TimeAgo } from "./ui";
35+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
36+
37+// --- Panels -------------------------------------------------------------------
38+
39+/** A titled section, with a count and a link to everything it summarises. */
40+export function Panel({
41+ id,
42+ title,
43+ icon,
44+ count,
45+ all,
46+ extra,
47+ children,
48+ className,
49+}: {
50+ id?: string;
51+ title: ReactNode;
52+ icon?: ReactNode;
53+ count?: number | null;
54+ all?: { to: string; label?: string } | null;
55+ extra?: ReactNode;
56+ children: ReactNode;
57+ className?: string;
58+}) {
59+ return (
60+ <section id={id} className={cn("scroll-mt-20", className)}>
61+ <div className="flex min-h-7 flex-wrap items-center gap-x-3 gap-y-2">
62+ <h2 className="flex items-center gap-2 text-sm font-semibold tracking-tight">
63+ {icon && <span className="text-faint">{icon}</span>}
64+ {title}
65+ {count != null && count > 0 && (
66+ <span className="rounded-full bg-line px-1.5 text-[0.6875rem] font-medium tabular-nums text-muted">{count}</span>
67+ )}
68+ </h2>
69+ <span className="grow" />
70+ {extra}
71+ {all && (
72+ <Link to={all.to} prefetch="intent" className="inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
73+ {all.label ?? "View all"}
74+ <ArrowRight size={12} />
75+ </Link>
76+ )}
77+ </div>
78+ <div className="mt-3">{children}</div>
79+ </section>
80+ );
81+}
82+
83+/** Says a section could not be loaded, without taking the page down. */
84+export function Unavailable({ what }: { what: string }) {
85+ return (
86+ <p className="flex items-center gap-2 rounded-xl border border-dashed border-line px-4 py-5 text-sm text-muted">
87+ <TriangleAlert size={14} className="text-warn" />
88+ {what} could not be loaded just now. It will be back on the next refresh.
89+ </p>
90+ );
91+}
92+
93+/** A quiet, dashed empty state with an optional call to action. */
94+export function Quiet({ children, action }: { children: ReactNode; action?: ReactNode }) {
95+ return (
96+ <div className="flex flex-wrap items-center gap-3 rounded-xl border border-dashed border-line px-4 py-5 text-sm text-muted">
97+ <span className="min-w-0 grow">{children}</span>
98+ {action}
99+ </div>
100+ );
101+}
102+
103+// --- Needs you ----------------------------------------------------------------
104+
105+const NEED: Record<NeedKind, { icon: ReactNode; tone: string; label: string }> = {
106+ limit: { icon: <CreditCard size={15} />, tone: "text-danger", label: "Usage" },
107+ deploy: { icon: <Rocket size={15} />, tone: "text-danger", label: "Production" },
108+ conflict: { icon: <Swords size={15} />, tone: "text-warn", label: "Conflict" },
109+ stalled: { icon: <Hand size={15} />, tone: "text-warn", label: "Stopped" },
110+ stuck: { icon: <TimerOff size={15} />, tone: "text-warn", label: "Quiet agent" },
111+ review: { icon: <Eye size={15} />, tone: "text-info", label: "Review" },
112+ checks: { icon: <Terminal size={15} />, tone: "text-danger", label: "Checks" },
113+ ready: { icon: <GitMerge size={15} />, tone: "text-accent", label: "Ready" },
114+};
115+
116+/** What is waiting on the viewer, most urgent first, each with its next step. */
117+export function NeedsList({ needs, limit = 8 }: { needs: Need[]; limit?: number }) {
118+ const [all, setAll] = useState(false);
119+ const shown = all ? needs : needs.slice(0, limit);
120+ return (
121+ <>
122+ <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
123+ {shown.map((need) => {
124+ const look = NEED[need.kind];
125+ return (
126+ <li key={need.key} className="flex items-start gap-3 px-4 py-3">
127+ <span className={cn("mt-0.5 shrink-0", look.tone)} title={look.label}>
128+ {look.icon}
129+ </span>
130+ <span className="min-w-0 grow">
131+ <Link to={need.to} prefetch="intent" className="block truncate text-sm font-medium hover:text-accent">
132+ {need.title}
133+ </Link>
134+ <span className="mt-0.5 block text-xs leading-5 text-muted">
135+ {need.where && <span className="font-mono text-faint">{need.where} · </span>}
136+ {need.detail}
137+ </span>
138+ </span>
139+ <span className="hidden shrink-0 pt-0.5 text-xs text-faint sm:block">
140+ <TimeAgo at={need.at} />
141+ </span>
142+ <Link
143+ to={need.to}
144+ prefetch="intent"
145+ className="shrink-0 rounded-md border border-line px-2.5 py-1 text-xs font-medium text-fg/85 transition-colors hover:border-line-strong hover:bg-raised hover:text-fg"
146+ >
147+ {need.action}
148+ </Link>
149+ </li>
150+ );
151+ })}
152+ </ul>
153+ {needs.length > limit && (
154+ <button type="button" onClick={() => setAll(!all)} className="mt-2 text-xs text-muted hover:text-fg">
155+ {all ? "Show fewer" : `Show ${needs.length - limit} more`}
156+ </button>
157+ )}
158+ </>
159+ );
160+}
161+
162+// --- Activity -----------------------------------------------------------------
163+
164+const VERB: Record<Verb, { icon: ReactNode; tone: string; text: (n: string) => string }> = {
165+ landed: { icon: <GitMerge size={14} />, tone: "text-merged", text: (n) => `landed ${n}` },
166+ opened_issue: { icon: <CircleDot size={14} />, tone: "text-accent", text: (n) => `opened ${n}` },
167+ closed_issue: { icon: <CircleSlash size={14} />, tone: "text-faint", text: (n) => `closed ${n}` },
168+ started: { icon: <Play size={14} />, tone: "text-merged", text: (n) => `started on ${n}` },
169+ ready: { icon: <GitPullRequest size={14} />, tone: "text-info", text: (n) => `marked ${n} ready for review` },
170+ checks_passed: { icon: <Terminal size={14} />, tone: "text-accent", text: (n) => `checks passed on ${n}` },
171+ checks_failed: { icon: <Terminal size={14} />, tone: "text-danger", text: (n) => `checks failed on ${n}` },
172+ approved: { icon: <CircleCheck size={14} />, tone: "text-accent", text: (n) => `approved ${n}` },
173+ changes_requested: { icon: <CircleSlash size={14} />, tone: "text-warn", text: (n) => `asked for changes on ${n}` },
174+ commented: { icon: <MessageSquare size={14} />, tone: "text-muted", text: (n) => `commented on ${n}` },
175+ asked: { icon: <MessageCircleQuestion size={14} />, tone: "text-merged", text: (n) => `asked the agent on ${n}` },
176+ deployed: { icon: <Rocket size={14} />, tone: "text-accent", text: () => "deployed production" },
177+ deploy_failed: { icon: <Rocket size={14} />, tone: "text-danger", text: () => "production build failed" },
178+ learned: { icon: <Brain size={14} />, tone: "text-merged", text: () => "learned" },
179+};
180+
181+function Refs({ numbers, base }: { numbers: number[]; base: string }) {
182+ const shown = numbers.slice(0, 4);
183+ return (
184+ <>
185+ {shown.map((number, index) => (
186+ <span key={number}>
187+ {index > 0 && (index === shown.length - 1 && numbers.length <= 4 ? " and " : ", ")}
188+ <Link to={`${base}/issues/${number}`} prefetch="intent" className="font-medium text-fg hover:underline">
189+ #{number}
190+ </Link>
191+ </span>
192+ ))}
193+ {numbers.length > 4 && ` and ${numbers.length - 4} more`}
194+ </>
195+ );
196+}
197+
198+/** One group as a sentence: "landed #4 and #3; started on #5". */
199+function GroupLine({ group }: { group: ActivityGroup }) {
200+ const base = `/${group.repo.namespace}/${group.repo.name}`;
201+ return (
202+ <>
203+ {group.parts.map((part, index) => {
204+ const look = VERB[part.verb];
205+ const [before, after] = look.text("\u0000").split("\u0000");
206+ return (
207+ <span key={part.verb}>
208+ {index > 0 && "; "}
209+ {part.verb === "learned" ? (
210+ <>
211+ learned{" "}
212+ <Link to={part.to ?? `${base}/memory`} className="text-fg-soft hover:text-fg">
213+ “{part.texts[0]}”
214+ </Link>
215+ {part.texts.length > 1 && ` and ${part.texts.length - 1} more`}
216+ </>
217+ ) : part.numbers.length > 0 ? (
218+ <>
219+ {before}
220+ <Refs numbers={part.numbers} base={base} />
221+ {after}
222+ </>
223+ ) : part.to ? (
224+ <Link to={part.to} className="hover:text-fg">
225+ {look.text("")}
226+ </Link>
227+ ) : (
228+ look.text("")
229+ )}
230+ </span>
231+ );
232+ })}
233+ </>
234+ );
235+}
236+
237+type Who = "all" | "agents" | "people";
238+
239+/**
240+ * What moved across projects, newest first, a burst of one actor's work
241+ * as one line, filterable by project and by who did it.
242+ */
243+export function ActivityFeed({
244+ groups,
245+ showRepo = true,
246+ limit = 14,
247+ empty,
248+}: {
249+ groups: ActivityGroup[];
250+ showRepo?: boolean;
251+ limit?: number;
252+ empty: ReactNode;
253+}) {
254+ const [project, setProject] = useState("all");
255+ const [who, setWho] = useState<Who>("all");
256+ const [person, setPerson] = useState("all");
257+ const [more, setMore] = useState(false);
258+ const projects = useMemo(
259+ () => [...new Set(groups.map((g) => `${g.repo.namespace}/${g.repo.name}`))].sort(),
260+ [groups],
261+ );
262+ const people = useMemo(
263+ () => [...new Set(groups.map((g) => g.actor).filter((a): a is string => a != null))].sort(),
264+ [groups],
265+ );
266+ const filtered = groups.filter(
267+ (g) =>
268+ (project === "all" || `${g.repo.namespace}/${g.repo.name}` === project) &&
269+ (person === "all" || g.actor === person) &&
270+ (who === "all" || (who === "agents" ? g.actor == null || isAgent(g.actor) : g.actor != null && !isAgent(g.actor))),
271+ );
272+ const shown = more ? filtered : filtered.slice(0, limit);
273+ return (
274+ <div>
275+ {groups.length > 0 && (
276+ <div className="mb-3 flex flex-wrap items-center gap-2">
277+ <div className="inline-flex rounded-lg border border-line bg-bg p-0.5 text-xs">
278+ {(["all", "agents", "people"] as const).map((value) => (
279+ <button
280+ key={value}
281+ type="button"
282+ onClick={() => setWho(value)}
283+ className={cn(
284+ "rounded-md px-2.5 py-1 font-medium capitalize transition-colors",
285+ who === value ? "bg-raised text-fg" : "text-muted hover:text-fg",
286+ )}
287+ >
288+ {value === "all" ? "Everyone" : value}
289+ </button>
290+ ))}
291+ </div>
292+ {showRepo && projects.length > 1 && (
293+ <Select value={project} onValueChange={setProject}>
294+ <SelectTrigger size="sm" className="w-auto max-w-56" aria-label="Project">
295+ <SelectValue />
296+ </SelectTrigger>
297+ <SelectContent>
298+ <SelectItem value="all">All projects</SelectItem>
299+ {projects.map((name) => (
300+ <SelectItem key={name} value={name}>
301+ {name.split("/")[1]}
302+ </SelectItem>
303+ ))}
304+ </SelectContent>
305+ </Select>
306+ )}
307+ {people.length > 1 && (
308+ <Select value={person} onValueChange={setPerson}>
309+ <SelectTrigger size="sm" className="w-auto max-w-48" aria-label="Who">
310+ <SelectValue />
311+ </SelectTrigger>
312+ <SelectContent>
313+ <SelectItem value="all">Anyone</SelectItem>
314+ {people.map((name) => (
315+ <SelectItem key={name} value={name} icon={<Avatar name={name} size={16} />}>
316+ {name}
317+ </SelectItem>
318+ ))}
319+ </SelectContent>
320+ </Select>
321+ )}
322+ </div>
323+ )}
324+ {shown.length === 0 ? (
325+ <Quiet>{groups.length === 0 ? empty : "Nothing matches these filters."}</Quiet>
326+ ) : (
327+ <ol className="relative space-y-0.5 before:absolute before:top-3 before:bottom-3 before:left-[0.9375rem] before:w-px before:bg-line">
328+ {shown.map((group) => {
329+ const look = VERB[group.parts[0].verb];
330+ const agent = group.actor == null || isAgent(group.actor);
331+ return (
332+ <li key={group.id} className="relative flex items-start gap-3 rounded-lg px-1 py-1.5 text-sm">
333+ <span
334+ className={cn(
335+ "relative z-10 mt-0.5 flex size-[1.375rem] shrink-0 items-center justify-center rounded-full bg-bg ring-1 ring-line",
336+ look.tone,
337+ )}
338+ >
339+ {look.icon}
340+ </span>
341+ <span className="min-w-0 grow leading-6 text-muted">
342+ {group.actor ? (
343+ <span className={cn("mr-1.5 inline-flex items-center gap-1.5 align-middle font-medium", agent ? "text-merged" : "text-fg")}>
344+ <Avatar name={group.actor} size={16} />
345+ {group.actor}
346+ </span>
347+ ) : (
348+ <span className="mr-1 text-fg-soft">g1t:</span>
349+ )}
350+ <GroupLine group={group} />
351+ {showRepo && (
352+ <Link
353+ to={`/${group.repo.namespace}/${group.repo.name}`}
354+ className="ml-1.5 font-mono text-xs text-faint hover:text-muted"
355+ >
356+ {group.repo.name}
357+ </Link>
358+ )}
359+ {group.count > 2 && <span className="ml-1.5 text-xs text-faint">· {group.count} events</span>}
360+ </span>
361+ <span className="shrink-0 pt-0.5 text-xs text-faint">
362+ <TimeAgo at={group.at} />
363+ </span>
364+ </li>
365+ );
366+ })}
367+ </ol>
368+ )}
369+ {filtered.length > limit && (
370+ <button type="button" onClick={() => setMore(!more)} className="mt-2 text-xs text-muted hover:text-fg">
371+ {more ? "Show fewer" : `Show ${filtered.length - limit} more`}
372+ </button>
373+ )}
374+ </div>
375+ );
376+}
377+
378+// --- Pulse --------------------------------------------------------------------
379+
380+const DAY_LABEL = (offset: number) => {
381+ const date = new Date(Date.now() - offset * 86_400_000);
382+ return date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", timeZone: "UTC" });
383+};
384+
385+/**
386+ * A week of one measure as a small line, the last day marked. Each day
387+ * has a hover target that says its value.
388+ */
389+export function Sparkline({
390+ values,
391+ format,
392+ width = 96,
393+ height = 28,
394+}: {
395+ values: number[];
396+ format: (value: number) => string;
397+ width?: number;
398+ height?: number;
399+}) {
400+ const points = sparkPoints(values, width, height, 3);
401+ if (points.length === 0) return null;
402+ const line = points.map(([x, y], i) => `${i ? "L" : "M"}${x},${y}`).join(" ");
403+ const area = `${line} L${points[points.length - 1][0]},${height} L${points[0][0]},${height} Z`;
404+ const [lastX, lastY] = points[points.length - 1];
405+ const slot = width / values.length;
406+ return (
407+ <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} className="overflow-visible text-accent" role="img" aria-label={values.map(format).join(", ")}>
408+ <path d={area} fill="currentColor" opacity={0.12} />
409+ <path d={line} fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
410+ <circle cx={lastX} cy={lastY} r={3} fill="currentColor" stroke="var(--color-surface)" strokeWidth={2} />
411+ {values.map((value, index) => (
412+ <rect key={index} x={index * slot} y={0} width={slot} height={height} fill="transparent" className="hover:fill-fg/5">
413+ <title>{`${DAY_LABEL(values.length - 1 - index)}: ${format(value)}`}</title>
414+ </rect>
415+ ))}
416+ </svg>
417+ );
418+}
419+
420+/** One measure of the week: a headline number, what it means, and its line. */
421+export function PulseTile({
422+ label,
423+ value,
424+ hint,
425+ values,
426+ format,
427+ to,
428+}: {
429+ label: string;
430+ value: string;
431+ hint?: string;
432+ values?: number[];
433+ format?: (value: number) => string;
434+ to?: string;
435+}) {
436+ const body = (
437+ <>
438+ <p className="truncate text-xs text-muted">{label}</p>
439+ <div className="mt-1 flex items-end justify-between gap-2">
440+ <p className="text-2xl font-semibold tracking-tight tabular-nums">{value}</p>
441+ {values && format && values.some((v) => v > 0) && (
442+ <span className="mb-1">
443+ <Sparkline values={values} format={format} width={56} height={22} />
444+ </span>
445+ )}
446+ </div>
447+ <p className="mt-1.5 truncate text-[0.6875rem] text-faint" title={hint}>
448+ {hint}
449+ </p>
450+ </>
451+ );
452+ const box = "block rounded-xl border border-line bg-surface p-4 transition-colors";
453+ return to ? (
454+ <Link to={to} className={`${box} hover:border-line-strong`}>
455+ {body}
456+ </Link>
457+ ) : (
458+ <div className={box}>{body}</div>
459+ );
460+}
461+
462+// --- Deploys ------------------------------------------------------------------
463+
464+const STRIP_TONE: Record<DeployStatus, string> = {
465+ ready: "bg-accent",
466+ // Served once, until a newer build or a take-down: it worked.
467+ replaced: "bg-accent/45",
468+ down: "bg-line-strong",
469+ failed: "bg-danger",
470+ building: "bg-warn animate-pulse",
471+ queued: "bg-line-strong animate-pulse",
472+ skipped: "bg-line",
473+};
474+
475+/** The last builds as a row of bars, oldest on the left; each links to its build. */
476+export function DeployStrip({
477+ builds,
478+ base,
479+ slots = 20,
480+}: {
481+ builds: { id: string; status: DeployStatus; kind: string; commit: string; createdAt: string }[];
482+ base: string;
483+ slots?: number;
484+}) {
485+ const shown = builds.slice(0, slots).reverse();
486+ return (
487+ <div className="flex h-8 items-end gap-[3px]" aria-label="Recent builds, oldest first">
488+ {Array.from({ length: slots - shown.length }, (_, i) => (
489+ <span key={`empty-${i}`} className="h-2 flex-1 rounded-sm bg-line/60" />
490+ ))}
491+ {shown.map((build) => (
492+ <Link
493+ key={build.id}
494+ to={`${base}/deployments/${build.id}`}
495+ title={`${build.kind === "production" ? "Production" : "Preview"} · ${build.commit.slice(0, 7)} · ${build.status} · ${new Date(build.createdAt).toLocaleString("en-US")}`}
496+ className={cn("flex-1 rounded-sm transition-opacity hover:opacity-80", STRIP_TONE[build.status], build.kind === "production" ? "h-8" : "h-5")}
497+ />
498+ ))}
499+ </div>
500+ );
501+}
502+
503+/** A small meter for a share, 0 to 1. */
504+export function Meter({ value, tone = "bg-accent" }: { value: number | null; tone?: string }) {
505+ return (
506+ <span className="block h-1.5 w-full overflow-hidden rounded-full bg-line">
507+ {value != null && <span className={cn("block h-full rounded-full", tone)} style={{ width: `${Math.round(value * 100)}%` }} />}
508+ </span>
509+ );
510+}
511+
512+export function percent(value: number | null): string {
513+ return value == null ? "—" : `${Math.round(value * 100)}%`;
514+}
515+
+7−1
1−import { GitBranch, Globe, Lock, Network, Rocket, Settings, Webhook } from "lucide-react";
1+import { Bot, GitBranch, Globe, Lock, Network, Rocket, Settings, ShieldCheck, Webhook } from "lucide-react";
22
33 import { TabLink } from "./ui";
44
2121 <TabLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
2222 Dependencies
2323 </TabLink>
24+ <TabLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
25+ Agents
26+ </TabLink>
27+ <TabLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
28+ Guardrails
29+ </TabLink>
2430 <TabLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
2531 Secrets and variables
2632 </TabLink>
+425−0
1+/**
2+ * Security, as a project's page shows it: findings by severity, the
3+ * secrets found in pushes and history with what was decided about each,
4+ * and vulnerable dependencies with the upgrade fixing each. The page
5+ * posts the intents in `routes/repo/security.tsx`'s action.
6+ */
7+import { Bot, CircleCheck, CircleDot, ExternalLink, GitPullRequest, KeyRound, Package, ShieldAlert, ShieldCheck } from "lucide-react";
8+import { useEffect, useRef, useState } from "react";
9+import { Link, useFetcher } from "react-router";
10+
11+import { SEVERITIES, type SecretFinding, type SecretStatus, type Severity, type SeverityCounts, type Vulnerability } from "@g1t/contracts";
12+
13+import { TimeAgo } from "./ui";
14+import { Badge, type BadgeTone } from "./ui/badge";
15+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
16+
17+type Done = { ok: boolean; error?: string } | undefined;
18+
19+const SEVERITY: Record<Severity, { label: string; tone: BadgeTone }> = {
20+ critical: { label: "Critical", tone: "danger" },
21+ high: { label: "High", tone: "warn" },
22+ medium: { label: "Medium", tone: "merged" },
23+ low: { label: "Low", tone: "info" },
24+ unknown: { label: "Unrated", tone: "neutral" },
25+};
26+
27+const STATUS: Record<SecretStatus, { label: string; tone: BadgeTone; about: string }> = {
28+ open: { label: "Open", tone: "danger", about: "In the repository's history. Rotate it, then mark it resolved." },
29+ blocked: { label: "Push blocked", tone: "warn", about: "A push carrying it was refused, so it never landed." },
30+ allowed: { label: "Allowed", tone: "neutral", about: "Not a real secret, so pushes carrying it go through." },
31+ resolved: { label: "Resolved", tone: "accent", about: "Rotated or removed." },
32+};
33+
34+export function SeverityBadge({ severity }: { severity: Severity }) {
35+ return <Badge tone={SEVERITY[severity].tone}>{SEVERITY[severity].label}</Badge>;
36+}
37+
38+/** Open findings by severity, one tile each. */
39+export function SeverityCountsGrid({ counts }: { counts: SeverityCounts }) {
40+ return (
41+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-5">
42+ {SEVERITIES.map((severity) => (
43+ <div key={severity} className="rounded-xl border border-line bg-surface px-4 py-3">
44+ <p className="text-xs text-muted">{SEVERITY[severity].label}</p>
45+ <p className={`mt-1 text-2xl font-semibold tabular-nums ${counts[severity] > 0 && severity === "critical" ? "text-danger" : ""}`}>
46+ {counts[severity]}
47+ </p>
48+ </div>
49+ ))}
50+ </div>
51+ );
52+}
53+
54+/** A compact row of severity counts, for a list of projects. */
55+export function SeverityCountsInline({ counts }: { counts: SeverityCounts }) {
56+ const shown = SEVERITIES.filter((severity) => counts[severity] > 0);
57+ if (shown.length === 0) {
58+ return (
59+ <span className="inline-flex items-center gap-1 text-xs text-accent">
60+ <ShieldCheck size={13} />
61+ Nothing open
62+ </span>
63+ );
64+ }
65+ return (
66+ <span className="flex flex-wrap gap-1.5">
67+ {shown.map((severity) => (
68+ <Badge key={severity} tone={SEVERITY[severity].tone}>
69+ {counts[severity]} {SEVERITY[severity].label.toLowerCase()}
70+ </Badge>
71+ ))}
72+ </span>
73+ );
74+}
75+
76+const TEXTAREA =
77+ "w-full rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim";
78+
79+/** Allow or resolve a secret, with the reason the record keeps. */
80+function Decide({ finding, decision, action }: { finding: SecretFinding; decision: "allow" | "resolve"; action: string }) {
81+ const fetcher = useFetcher<Done>();
82+ const [open, setOpen] = useState(false);
83+ useEffect(() => {
84+ if (fetcher.state === "idle" && fetcher.data?.ok) setOpen(false);
85+ }, [fetcher.state, fetcher.data]);
86+ const allow = decision === "allow";
87+ return (
88+ <Dialog open={open} onOpenChange={setOpen}>
89+ <DialogTrigger
90+ className={`rounded-md border px-2.5 py-1 text-xs font-medium transition-colors ${
91+ allow ? "border-line text-muted hover:border-line-strong hover:text-fg" : "border-accent/40 text-accent hover:bg-accent/10"
92+ }`}
93+ >
94+ {allow ? "Allow" : "Resolve"}
95+ </DialogTrigger>
96+ <DialogContent>
97+ <DialogHeader>
98+ <DialogTitle>{allow ? `Allow ${finding.label}?` : `Mark ${finding.label} resolved?`}</DialogTitle>
99+ <DialogDescription>
100+ {allow
101+ ? finding.status === "blocked"
102+ ? "Say why it is not a real secret. The push it stopped can then be pushed again as it is, and the record keeps your name and reason."
103+ : "Say why it is not a real secret. The record keeps your name and reason."
104+ : "Rotate it with whoever issued it first: removing it from the code leaves it in history. The record keeps your name and reason."}
105+ </DialogDescription>
106+ </DialogHeader>
107+ <fetcher.Form method="post" action={action} className="space-y-3">
108+ <input type="hidden" name="intent" value="decide" />
109+ <input type="hidden" name="id" value={finding.id} />
110+ <input type="hidden" name="decision" value={decision} />
111+ <p className="font-mono text-xs text-muted">
112+ {finding.path}:{finding.line} · {finding.preview}
113+ </p>
114+ <textarea
115+ name="reason"
116+ required
117+ rows={3}
118+ maxLength={500}
119+ placeholder={allow ? "A fake key in a test fixture." : "Rotated in the AWS console; the old key is disabled."}
120+ className={TEXTAREA}
121+ />
122+ <div className="flex justify-end">
123+ <button
124+ type="submit"
125+ disabled={fetcher.state !== "idle"}
126+ className="rounded-md bg-fg px-3.5 py-2 text-sm font-medium text-bg hover:bg-white disabled:opacity-50"
127+ >
128+ {allow ? "Allow" : "Mark resolved"}
129+ </button>
130+ </div>
131+ {fetcher.data?.error && <p className="text-sm text-danger">{fetcher.data.error}</p>}
132+ </fetcher.Form>
133+ </DialogContent>
134+ </Dialog>
135+ );
136+}
137+
138+function SecretItem({ finding, base, action, focused }: { finding: SecretFinding; base: string; action: string; focused: boolean }) {
139+ const reopen = useFetcher<Done>();
140+ const ref = useRef<HTMLLIElement>(null);
141+ useEffect(() => {
142+ if (focused) ref.current?.scrollIntoView({ block: "center" });
143+ }, [focused]);
144+ const status = STATUS[finding.status];
145+ const landed = finding.source === "history" || finding.status === "open";
146+ return (
147+ <li ref={ref} className={`flex flex-col gap-3 px-4 py-3 sm:flex-row sm:items-start ${focused ? "bg-accent/5 ring-1 ring-accent/40 ring-inset" : ""}`}>
148+ <KeyRound size={15} className="mt-0.5 hidden shrink-0 text-muted sm:block" />
149+ <div className="min-w-0 grow">
150+ <div className="flex flex-wrap items-center gap-2">
151+ <span className="text-sm font-medium first-letter:uppercase">{finding.label}</span>
152+ <Badge tone={status.tone} title={status.about}>
153+ {status.label}
154+ </Badge>
155+ </div>
156+ <p className="mt-1 truncate font-mono text-xs">
157+ {landed ? (
158+ <Link to={`${base}/blob/${finding.commit}/${finding.path}#L${finding.line}`} className="text-fg-soft hover:text-fg hover:underline">
159+ {finding.path}:{finding.line}
160+ </Link>
161+ ) : (
162+ <span className="text-fg-soft">
163+ {finding.path}:{finding.line}
164+ </span>
165+ )}
166+ <span className="text-faint"> · {finding.preview}</span>
167+ </p>
168+ <p className="mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-faint">
169+ <span>
170+ {finding.source === "push" ? "in a push" : "in history"}
171+ {finding.foundBy && <> by {finding.foundBy}</>}, commit{" "}
172+ {landed ? (
173+ <Link to={`${base}/commit/${finding.commit}`} className="font-mono hover:text-fg">
174+ {finding.commit.slice(0, 7)}
175+ </Link>
176+ ) : (
177+ <span className="font-mono">{finding.commit.slice(0, 7)}</span>
178+ )}
179+ </span>
180+ <span>
181+ found <TimeAgo at={finding.foundAt} />
182+ </span>
183+ {finding.decidedBy && finding.decidedAt && (
184+ <span>
185+ {finding.status === "allowed" ? "allowed" : "resolved"} by {finding.decidedBy} <TimeAgo at={finding.decidedAt} />
186+ {finding.reason && <>: “{finding.reason}”</>}
187+ </span>
188+ )}
189+ </p>
190+ {reopen.data?.error && <p className="mt-1.5 text-xs text-danger">{reopen.data.error}</p>}
191+ </div>
192+ <div className="flex shrink-0 items-center gap-1.5">
193+ {finding.status === "open" || finding.status === "blocked" ? (
194+ <>
195+ <Decide finding={finding} decision="allow" action={action} />
196+ {finding.status === "open" && <Decide finding={finding} decision="resolve" action={action} />}
197+ </>
198+ ) : (
199+ <button
200+ type="button"
201+ disabled={reopen.state !== "idle"}
202+ onClick={() => reopen.submit({ intent: "decide", id: finding.id, decision: "reopen" }, { method: "post", action })}
203+ className="rounded-md border border-line px-2.5 py-1 text-xs font-medium text-muted transition-colors hover:border-line-strong hover:text-fg disabled:opacity-50"
204+ >
205+ Reopen
206+ </button>
207+ )}
208+ </div>
209+ </li>
210+ );
211+}
212+
213+export function SecretsList({
214+ secrets,
215+ base,
216+ action,
217+ focus,
218+}: {
219+ secrets: SecretFinding[];
220+ base: string;
221+ action: string;
222+ focus: string | null;
223+}) {
224+ if (secrets.length === 0) {
225+ return (
226+ <div className="rounded-xl border border-dashed border-line px-6 py-10 text-center">
227+ <ShieldCheck size={22} className="mx-auto text-accent" />
228+ <p className="mt-2 font-medium">No secrets found</p>
229+ <p className="mt-1 text-sm text-muted">
230+ Pushes that add a key or a token are refused before they land, and the history is scanned once in the background.
231+ </p>
232+ </div>
233+ );
234+ }
235+ return (
236+ <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
237+ {secrets.map((finding) => (
238+ <SecretItem key={finding.id} finding={finding} base={base} action={action} focused={finding.id === focus} />
239+ ))}
240+ </ul>
241+ );
242+}
243+
244+/** Where the upgrade issue for a package stands, as the page loads it. */
245+export type UpgradeFix = {
246+ number: number;
247+ state: "open" | "closed";
248+ /** The newest pull request for the issue, if any. */
249+ pull: { number: number; status: "draft" | "open" | "merged" | "closed"; agent: string | null } | null;
250+ resolvedBy: number | null;
251+};
252+
253+function FixLink({ issue, fix, base }: { issue: number | null; fix: UpgradeFix | undefined; base: string }) {
254+ if (issue == null) return <span className="text-xs text-faint">No upgrade issue</span>;
255+ const pull = fix?.pull;
256+ return (
257+ <span className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
258+ <Link to={`${base}/issues/${issue}`} className="inline-flex items-center gap-1 text-fg-soft hover:text-fg">
259+ {fix?.state === "closed" ? <CircleCheck size={12} className="text-merged" /> : <CircleDot size={12} className="text-accent" />}#{issue}
260+ </Link>
261+ {pull && (
262+ <Link to={`${base}/pull/${pull.number}`} className="inline-flex items-center gap-1 text-muted hover:text-fg">
263+ {pull.agent ? <Bot size={12} /> : <GitPullRequest size={12} />}
264+ #{pull.number} {pull.status === "draft" ? "in progress" : pull.status}
265+ </Link>
266+ )}
267+ </span>
268+ );
269+}
270+
271+type PackageGroup = { key: string; ecosystem: string; name: string; vulns: Vulnerability[] };
272+
273+function groups(vulnerabilities: Vulnerability[]): PackageGroup[] {
274+ const map = new Map<string, PackageGroup>();
275+ for (const vuln of vulnerabilities) {
276+ const key = `${vuln.ecosystem}:${vuln.package}`;
277+ const group = map.get(key) ?? { key, ecosystem: vuln.ecosystem, name: vuln.package, vulns: [] };
278+ group.vulns.push(vuln);
279+ map.set(key, group);
280+ }
281+ return [...map.values()];
282+}
283+
284+function worst(vulns: Vulnerability[]): Severity {
285+ return SEVERITIES.find((severity) => vulns.some((vuln) => vuln.severity === severity)) ?? "unknown";
286+}
287+
288+export function VulnerabilityList({
289+ vulnerabilities,
290+ fixes,
291+ base,
292+}: {
293+ vulnerabilities: Vulnerability[];
294+ fixes: Record<number, UpgradeFix>;
295+ base: string;
296+}) {
297+ const open = groups(vulnerabilities.filter((vuln) => vuln.status === "open"));
298+ const fixed = groups(vulnerabilities.filter((vuln) => vuln.status === "fixed"));
299+ return (
300+ <div className="space-y-6">
301+ {open.length === 0 ? (
302+ <div className="rounded-xl border border-dashed border-line px-6 py-10 text-center">
303+ <ShieldCheck size={22} className="mx-auto text-accent" />
304+ <p className="mt-2 font-medium">No known vulnerabilities</p>
305+ <p className="mt-1 text-sm text-muted">
306+ Every package the lockfiles resolve is checked against the OSV database on each push to the default branch, and daily.
307+ </p>
308+ </div>
309+ ) : (
310+ <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
311+ {open.map((group) => (
312+ <PackageItem key={group.key} group={group} fixes={fixes} base={base} />
313+ ))}
314+ </ul>
315+ )}
316+ {fixed.length > 0 && (
317+ <details className="group">
318+ <summary className="cursor-pointer text-sm text-muted hover:text-fg">
319+ Fixed ({fixed.reduce((sum, group) => sum + group.vulns.length, 0)})
320+ </summary>
321+ <ul className="mt-3 divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface opacity-80">
322+ {fixed.map((group) => (
323+ <PackageItem key={group.key} group={group} fixes={fixes} base={base} />
324+ ))}
325+ </ul>
326+ </details>
327+ )}
328+ </div>
329+ );
330+}
331+
332+function PackageItem({ group, fixes, base }: { group: PackageGroup; fixes: Record<number, UpgradeFix>; base: string }) {
333+ const first = group.vulns[0];
334+ const versions = [...new Set(group.vulns.map((vuln) => vuln.version))];
335+ const targets = group.vulns.map((vuln) => vuln.fixedVersion).filter((version): version is string => !!version);
336+ const manifests = [...new Set(group.vulns.map((vuln) => vuln.manifest))];
337+ const issue = group.vulns.find((vuln) => vuln.issue != null)?.issue ?? null;
338+ const advisories = [...new Map(group.vulns.map((vuln) => [vuln.advisory, vuln])).values()];
339+ return (
340+ <li className="px-4 py-3">
341+ <div className="flex flex-col gap-2 sm:flex-row sm:items-start">
342+ <Package size={15} className="mt-0.5 hidden shrink-0 text-muted sm:block" />
343+ <div className="min-w-0 grow">
344+ <div className="flex flex-wrap items-center gap-2">
345+ <span className="font-mono text-sm font-medium">{group.name}</span>
346+ <span className="font-mono text-xs text-muted">{versions.join(", ")}</span>
347+ <Badge>{first.ecosystem}</Badge>
348+ <SeverityBadge severity={worst(group.vulns)} />
349+ </div>
350+ <p className="mt-1 text-xs text-faint">
351+ {targets.length > 0 ? <>Fixed in {targets.sort().at(-1)}</> : "No fixed version yet"} · locked in{" "}
352+ <span className="font-mono">{manifests.join(", ")}</span>
353+ </p>
354+ </div>
355+ <FixLink issue={issue} fix={issue != null ? fixes[issue] : undefined} base={base} />
356+ </div>
357+ <ul className="mt-2 space-y-1 sm:pl-7">
358+ {advisories.map((vuln) => (
359+ <li key={vuln.advisory} className="flex flex-wrap items-baseline gap-x-2 text-xs">
360+ <a
361+ href={`https://osv.dev/vulnerability/${vuln.osvId}`}
362+ target="_blank"
363+ rel="noreferrer"
364+ className="inline-flex items-center gap-1 font-mono text-fg-soft hover:text-fg"
365+ >
366+ {vuln.advisory}
367+ <ExternalLink size={10} />
368+ </a>
369+ <span className="text-muted">{SEVERITY[vuln.severity].label.toLowerCase()}</span>
370+ <span className="min-w-0 truncate text-muted">{vuln.summary}</span>
371+ </li>
372+ ))}
373+ </ul>
374+ </li>
375+ );
376+}
377+
378+export function ScanSummary({
379+ scan,
380+}: {
381+ scan: { history: string; commitsScanned: number; historyFinishedAt: string | null; dependenciesScannedAt: string | null; dependenciesError: string | null; lockfiles: string[] };
382+}) {
383+ const history =
384+ scan.history === "done" ? (
385+ <>
386+ History scanned: {scan.commitsScanned.toLocaleString()} commits
387+ {scan.historyFinishedAt && (
388+ <>
389+ , <TimeAgo at={scan.historyFinishedAt} />
390+ </>
391+ )}
392+ </>
393+ ) : scan.history === "stopped" ? (
394+ "History scan paused: the workspace reached its spending limit"
395+ ) : scan.history === "running" ? (
396+ `Scanning history: ${scan.commitsScanned.toLocaleString()} commits so far`
397+ ) : (
398+ "History scan queued"
399+ );
400+ return (
401+ <div className="flex flex-wrap gap-x-5 gap-y-1 text-xs text-muted">
402+ <span className="inline-flex items-center gap-1.5">
403+ <KeyRound size={12} />
404+ {history}
405+ </span>
406+ <span className="inline-flex items-center gap-1.5">
407+ <Package size={12} />
408+ {scan.dependenciesScannedAt ? (
409+ <>
410+ Dependencies read <TimeAgo at={scan.dependenciesScannedAt} />
411+ {scan.lockfiles.length > 0 ? <> from {scan.lockfiles.join(", ")}</> : " (no lockfiles found)"}
412+ </>
413+ ) : (
414+ "Dependencies not read yet"
415+ )}
416+ </span>
417+ {scan.dependenciesError && (
418+ <span className="inline-flex items-center gap-1.5 text-warn">
419+ <ShieldAlert size={12} />
420+ {scan.dependenciesError}
421+ </span>
422+ )}
423+ </div>
424+ );
425+}
+19−4
3636 PlayCircle,
3737 Bot,
3838 Brain,
39+ Network,
3940 ShieldCheck,
4041 Rocket,
4142 X,
363364 }
364365
365366 /** A workspace's settings pages, which the sidebar slides over to. */
366−const SETTINGS_PAGE = /^\/([^/]+)\/-\/(settings|people|tokens|billing|integrations|webhooks|secrets)(\/|$)/;
367+const SETTINGS_PAGE = /^\/([^/]+)\/-\/(settings|people|tokens|billing|integrations|webhooks|secrets|audit)(\/|$)/;
367368
368369 /**
369370 * The sidebar's menus are two layers, the way a phone pushes a screen: the
413414 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
414415 Secrets and variables
415416 </SidebarLink>
417+ <SidebarLink to={`/${slug}/-/guardrails`} icon={<ShieldCheck size={15} />}>
418+ Guardrails
419+ </SidebarLink>
416420 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
417421 Webhooks
418422 </SidebarLink>
423+ <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
424+ Audit log
425+ </SidebarLink>
419426 </SidebarGroup>
420427 </nav>
421428 );
500507 ) : null}
501508 <SidebarSoonLink to={`${base}/soon/logs`} also={soonPaths(base, "Observability")} icon={<Activity size={15} />} about="Logs, errors, uptime and analytics of the project's deployed apps.">
502509 Observability
503− </SidebarSoonLink>
504− <SidebarSoonLink to={`${base}/soon/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />} about="Findings, secret scanning, dependency updates and code scanning, each fixed by an agent.">
505− Security
506510 </SidebarSoonLink>
511+ {repo.member ? (
512+ <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
513+ Security
514+ </SidebarLink>
515+ ) : null}
507516 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
508517 Insights
509518 </SidebarSoonLink>
679688 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}>
680689 Agent fleet
681690 </SidebarLink>
691+ <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}>
692+ Context
693+ </SidebarLink>
682694 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}>
683695 Memory
684696 </SidebarLink>
697+ <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}>
698+ Security
699+ </SidebarLink>
685700 {roadmapIn("Workspace").map((item) => (
686701 <SidebarSoonLink
687702 key={item.key}
+30−0
1+import type { ComponentProps } from "react";
2+
3+import { cn } from "../../lib/cn";
4+
5+// shadcn/ui's badge, styled with g1t's tokens: a small outlined label whose
6+// tone says how much it matters.
7+
8+export type BadgeTone = "neutral" | "accent" | "info" | "merged" | "warn" | "danger";
9+
10+const TONES: Record<BadgeTone, string> = {
11+ neutral: "border-line text-muted",
12+ accent: "border-accent/40 bg-accent/10 text-accent",
13+ info: "border-info/40 bg-info/10 text-info",
14+ merged: "border-merged/40 bg-merged/10 text-merged",
15+ warn: "border-warn/40 bg-warn/10 text-warn",
16+ danger: "border-danger/40 bg-danger/10 text-danger",
17+};
18+
19+export function Badge({ tone = "neutral", className, ...props }: ComponentProps<"span"> & { tone?: BadgeTone }) {
20+ return (
21+ <span
22+ className={cn(
23+ "inline-flex shrink-0 items-center gap-1 rounded-full border px-2 py-px text-[0.6875rem] font-medium whitespace-nowrap",
24+ TONES[tone],
25+ className,
26+ )}
27+ {...props}
28+ />
29+ );
30+}
+3−2
1515
1616 import { repoAt } from "../lib/markdown-plugins";
1717 import { Markdown } from "./markdown";
18−import { Avatar, Button, Textarea, TimeAgo } from "./ui";
18+import { MentionTextarea } from "./mention-textarea";
19+import { Avatar, Button, TimeAgo } from "./ui";
1920 import { CheckboxOption } from "./ui/checkbox";
2021
2122 /** Hues for the labels every repository starts with. */
426427 </span>
427428 <Form method="post" className="min-w-0 grow space-y-2" key={resetKey}>
428429 <input type="hidden" name="action" value="comment" />
429− <Textarea
430+ <MentionTextarea
430431 name="body"
431432 rows={3}
432433 placeholder={
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

This change is too large to show in full.