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.
| 989 | 989 | "futures-util", | |
| 990 | 990 | "g1t-contracts", | |
| 991 | 991 | "g1t-kit", | |
| 992 | + | "g1t-scan", | |
| 992 | 993 | "serde", | |
| 993 | 994 | "serde_json", | |
| 994 | 995 | "similar", | |
| 1012 | 1013 | ] | |
| 1013 | 1014 | ||
| 1014 | 1015 | [[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]] | |
| 1015 | 1028 | name = "g1t-secrets" | |
| 1016 | 1029 | version = "0.1.0" | |
| 1017 | 1030 | dependencies = [ | |
| 1024 | 1037 | ] | |
| 1025 | 1038 | ||
| 1026 | 1039 | [[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]] | |
| 1027 | 1054 | name = "g1t-sshd" | |
| 1028 | 1055 | version = "0.1.0" | |
| 1029 | 1056 | dependencies = [ | |
| 1752 | 1779 | ||
| 1753 | 1780 | [[package]] | |
| 1754 | 1781 | 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" | |
| 1755 | 1788 | version = "0.2.1" | |
| 1756 | 1789 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1757 | 1790 | checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" | |
| 2375 | 2408 | ||
| 2376 | 2409 | [[package]] | |
| 2377 | 2410 | 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" | |
| 2378 | 2424 | version = "0.8.4" | |
| 2379 | 2425 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2380 | 2426 | checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" | |
| 2381 | 2427 | dependencies = [ | |
| 2382 | − | "openssl-probe", | |
| 2428 | + | "openssl-probe 0.2.1", | |
| 2383 | 2429 | "rustls-pki-types", | |
| 2384 | 2430 | "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", | |
| 2386 | 2441 | ] | |
| 2387 | 2442 | ||
| 2388 | 2443 | [[package]] | |
| 2407 | 2462 | "log", | |
| 2408 | 2463 | "once_cell", | |
| 2409 | 2464 | "rustls", | |
| 2410 | − | "rustls-native-certs", | |
| 2465 | + | "rustls-native-certs 0.8.4", | |
| 2411 | 2466 | "rustls-platform-verifier-android", | |
| 2412 | 2467 | "rustls-webpki", | |
| 2413 | − | "security-framework", | |
| 2468 | + | "security-framework 3.7.0", | |
| 2414 | 2469 | "security-framework-sys", | |
| 2415 | 2470 | "webpki-root-certs", | |
| 2416 | 2471 | "windows-sys 0.61.2", | |
| 2508 | 2563 | ||
| 2509 | 2564 | [[package]] | |
| 2510 | 2565 | 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" | |
| 2511 | 2579 | version = "3.7.0" | |
| 2512 | 2580 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2513 | 2581 | checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" | |
| 3223 | 3291 | "log", | |
| 3224 | 3292 | "once_cell", | |
| 3225 | 3293 | "rustls", | |
| 3294 | + | "rustls-native-certs 0.7.3", | |
| 3226 | 3295 | "rustls-pki-types", | |
| 3227 | 3296 | "serde", | |
| 3228 | 3297 | "serde_json", |
| 1 | 1 | [workspace] | |
| 2 | 2 | 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"] | |
| 4 | 4 | ||
| 5 | 5 | [workspace.package] | |
| 6 | 6 | edition = "2024" | |
| 11 | 11 | g1t-actions = { path = "crates/actions" } | |
| 12 | 12 | g1t-contracts = { path = "crates/contracts" } | |
| 13 | 13 | g1t-kit = { path = "crates/kit" } | |
| 14 | + | g1t-scan = { path = "crates/scan" } | |
| 14 | 15 | g1t-secrets = { path = "crates/secrets" } | |
| 15 | 16 | serde = { version = "1", features = ["derive"] } | |
| 16 | 17 | serde_json = "1" |
| 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 | + | } |
| 94 | 94 | } | |
| 95 | 95 | ||
| 96 | 96 | 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)) | |
| 98 | 98 | } | |
| 99 | 99 | ||
| 100 | 100 | fn valid_name(name: &str) -> bool { | |
| 161 | 161 | .into_iter() | |
| 162 | 162 | .map(|(_, meta)| json!({ "name": meta.name, "size": meta.size })) | |
| 163 | 163 | .collect(); | |
| 164 | − | Response::from_json(&listed) | |
| 164 | + | crate::reply(&listed) | |
| 165 | 165 | } | |
| 166 | 166 | (_, what) if what.starts_with("artifacts/") => { | |
| 167 | 167 | let name = decode(&what["artifacts/".len()..]); | |
| 175 | 175 | return error(413, "Artifacts are at most 60 MB."); | |
| 176 | 176 | } | |
| 177 | 177 | 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() })); | |
| 179 | 179 | } | |
| 180 | 180 | match get(&kv, &base).await? { | |
| 181 | 181 | Some(bytes) => Response::from_bytes(bytes), | |
| 191 | 191 | let base = format!("c/{repo}/{key}"); | |
| 192 | 192 | // A key is written once, as on GitHub. | |
| 193 | 193 | 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." })); | |
| 195 | 195 | } | |
| 196 | 196 | let bytes = request.bytes().await?; | |
| 197 | 197 | if bytes.len() > MAX_BYTES { | |
| 198 | 198 | return error(413, "Cache entries are at most 60 MB."); | |
| 199 | 199 | } | |
| 200 | 200 | put(&kv, &base, &key, &bytes, CACHE_TTL).await?; | |
| 201 | − | return Response::from_json(&json!({ "saved": true })); | |
| 201 | + | return crate::reply(&json!({ "saved": true })); | |
| 202 | 202 | } | |
| 203 | 203 | // The exact key, else the newest entry under each restore key. | |
| 204 | 204 | let exact = format!("c/{repo}/{key}"); |
| 4 | 4 | //! operations (see [`operations::Op`]), which call the services that own | |
| 5 | 5 | //! the data. This Worker holds none. | |
| 6 | 6 | ||
| 7 | + | mod audit; | |
| 7 | 8 | mod blobs; | |
| 8 | 9 | mod mcp; | |
| 9 | 10 | mod oauth; | |
| 10 | 11 | mod openapi; | |
| 11 | 12 | mod operations; | |
| 12 | 13 | mod renamed; | |
| 14 | + | #[cfg(test)] | |
| 15 | + | mod responses; | |
| 13 | 16 | mod rest; | |
| 14 | 17 | ||
| 15 | 18 | use g1t_contracts::billing::FinishRunArgs; | |
| 22 | 25 | }; | |
| 23 | 26 | use g1t_contracts::identity::AgentScope; | |
| 24 | 27 | use g1t_contracts::{Failure, FailureCode, Outcome, PrincipalKind, Viewer}; | |
| 28 | + | use g1t_kit::wire; | |
| 25 | 29 | use serde_json::{Value, json}; | |
| 26 | 30 | use worker::{Context, Env, Method, Request, Response, Result, event}; | |
| 27 | 31 | ||
| 42 | 46 | } | |
| 43 | 47 | } | |
| 44 | 48 | ||
| 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 | + | ||
| 45 | 64 | /// An error in the shape every endpoint uses. | |
| 46 | 65 | 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())) | |
| 48 | 67 | } | |
| 49 | 68 | ||
| 50 | 69 | fn fail(code: FailureCode, message: &str) -> Result<Response> { | |
| 138 | 157 | .collect(); | |
| 139 | 158 | let body = request.text().await.unwrap_or_default(); | |
| 140 | 159 | 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)); | |
| 142 | 161 | } | |
| 143 | 162 | let received: g1t_contracts::integrations::Received = g1t_kit::call( | |
| 144 | 163 | &services.integrations, | |
| 146 | 165 | &json!({ "id": id, "headers": headers, "body": body }), | |
| 147 | 166 | ) | |
| 148 | 167 | .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)) | |
| 150 | 169 | } | |
| 151 | 170 | ||
| 152 | 171 | async fn receive_stripe(request: &mut Request, env: &Env) -> Result<Response> { | |
| 153 | 172 | let signature = request.headers().get("stripe-signature")?.unwrap_or_default(); | |
| 154 | 173 | let payload = request.text().await.unwrap_or_default(); | |
| 155 | 174 | 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)); | |
| 157 | 176 | } | |
| 158 | 177 | let handled: g1t_contracts::Outcome<bool> = g1t_kit::call( | |
| 159 | 178 | &env.service("BILLING")?, | |
| 164 | 183 | // A refusal is a 400, so Stripe shows it as failed; anything handled, | |
| 165 | 184 | // or already handled, is a 200, so Stripe stops sending it. | |
| 166 | 185 | Ok(match handled { | |
| 167 | − | g1t_contracts::Outcome::Ok(_) => Response::from_json(&json!({ "received": true }))?, | |
| 186 | + | g1t_contracts::Outcome::Ok(_) => reply(&json!({ "received": true }))?, | |
| 168 | 187 | 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) | |
| 170 | 189 | } | |
| 171 | 190 | }) | |
| 172 | 191 | } | |
| 181 | 200 | }, | |
| 182 | 201 | ) | |
| 183 | 202 | .await?; | |
| 184 | − | Response::from_json(&json!({ | |
| 203 | + | reply(&json!({ | |
| 185 | 204 | "device_code": started.device_code, | |
| 186 | 205 | "user_code": started.user_code, | |
| 187 | 206 | "verification_uri": "https://g1t.sh/device", | |
| 201 | 220 | }, | |
| 202 | 221 | ) | |
| 203 | 222 | .await?; | |
| 204 | − | Response::from_json(&match claim { | |
| 223 | + | reply(&match claim { | |
| 205 | 224 | DeviceClaim::Approved { token, user } => json!({ | |
| 206 | 225 | "status": "approved", | |
| 207 | 226 | "token": token, | |
| 236 | 255 | ) | |
| 237 | 256 | .await?; | |
| 238 | 257 | match reported { | |
| 239 | − | Outcome::Ok(run) => Response::from_json(&json!({ "status": run.status })), | |
| 258 | + | Outcome::Ok(run) => reply(&json!({ "status": run.status })), | |
| 240 | 259 | Outcome::Fail(refused) => failure(&refused), | |
| 241 | 260 | } | |
| 242 | 261 | } | |
| 264 | 283 | ) | |
| 265 | 284 | .await?; | |
| 266 | 285 | match reported { | |
| 267 | − | Outcome::Ok(state) => Response::from_json(&json!({ "state": state })), | |
| 286 | + | Outcome::Ok(state) => reply(&json!({ "state": state })), | |
| 268 | 287 | Outcome::Fail(refused) => failure(&refused), | |
| 269 | 288 | } | |
| 270 | 289 | } | |
| 289 | 308 | ) | |
| 290 | 309 | .await?; | |
| 291 | 310 | match reported { | |
| 292 | − | Outcome::Ok(state) => Response::from_json(&json!({ "mergeable": state })), | |
| 311 | + | Outcome::Ok(state) => reply(&json!({ "mergeable": state })), | |
| 293 | 312 | Outcome::Fail(refused) => failure(&refused), | |
| 294 | 313 | } | |
| 295 | 314 | } | |
| 317 | 336 | ) | |
| 318 | 337 | .await?; | |
| 319 | 338 | match reported { | |
| 320 | − | Outcome::Ok(_) => Response::from_json(&json!({ "recorded": true })), | |
| 339 | + | Outcome::Ok(_) => reply(&json!({ "recorded": true })), | |
| 321 | 340 | Outcome::Fail(refused) => failure(&refused), | |
| 322 | 341 | } | |
| 323 | 342 | } | |
| 343 | 362 | ) | |
| 344 | 363 | .await?; | |
| 345 | 364 | match reported { | |
| 346 | − | Outcome::Ok(_) => Response::from_json(&json!({ "recorded": true })), | |
| 365 | + | Outcome::Ok(_) => reply(&json!({ "recorded": true })), | |
| 347 | 366 | Outcome::Fail(refused) => failure(&refused), | |
| 348 | 367 | } | |
| 349 | 368 | } | |
| 369 | 388 | ) | |
| 370 | 389 | .await?; | |
| 371 | 390 | match charged { | |
| 372 | − | Outcome::Ok(_) => Response::from_json(&json!({ "recorded": true })), | |
| 391 | + | Outcome::Ok(_) => reply(&json!({ "recorded": true })), | |
| 373 | 392 | Outcome::Fail(refused) => failure(&refused), | |
| 374 | 393 | } | |
| 375 | 394 | } | |
| 425 | 444 | // A fresh response: a fetched one's headers cannot be changed, and | |
| 426 | 445 | // every response gets the API's own on the way out. | |
| 427 | 446 | 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)? | |
| 429 | 453 | .with_status(status) | |
| 430 | 454 | .with_headers({ | |
| 431 | 455 | let headers = worker::Headers::new(); | |
| 448 | 472 | Ok(viewer) => viewer, | |
| 449 | 473 | Err(refused) => return Ok(refused), | |
| 450 | 474 | }; | |
| 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) { | |
| 453 | 481 | let header = request.headers().get("authorization")?.unwrap_or_default(); | |
| 454 | 482 | let token = header.split_once(' ').map(|(_, token)| token.trim()).unwrap_or_default(); | |
| 455 | 483 | let scope: Option<AgentScope> = g1t_kit::call( | |
| 474 | 502 | } | |
| 475 | 503 | ||
| 476 | 504 | match (method, path.trim_end_matches('/')) { | |
| 477 | − | ("GET", "") => return Response::from_json(&index()), | |
| 505 | + | ("GET", "") => return reply(&index()), | |
| 478 | 506 | ("GET", "/openapi.json") => return Response::from_json(&openapi::document()), | |
| 479 | 507 | // A run's artifacts: listed, or one downloaded. | |
| 480 | 508 | ("GET", path) if path.starts_with("/repos/") && path.contains("/actions/runs/") && path.contains("/artifacts") => { | |
| 489 | 517 | ) | |
| 490 | 518 | .await?; | |
| 491 | 519 | match seen { | |
| 492 | − | Outcome::Ok(_) => Response::from_json(&blobs::of_run(env, run).await?), | |
| 520 | + | Outcome::Ok(_) => reply(&blobs::of_run(env, run).await?), | |
| 493 | 521 | Outcome::Fail(refused) => failure(&refused), | |
| 494 | 522 | } | |
| 495 | 523 | } | |
| 509 | 537 | ) | |
| 510 | 538 | .await?; | |
| 511 | 539 | return match located { | |
| 512 | − | Outcome::Ok(value) => Response::from_json(&value), | |
| 540 | + | Outcome::Ok(value) => reply(&value), | |
| 513 | 541 | Outcome::Fail(refused) => failure(&refused), | |
| 514 | 542 | }; | |
| 515 | 543 | } | |
| 537 | 565 | ) | |
| 538 | 566 | .await?; | |
| 539 | 567 | 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)), | |
| 541 | 571 | Outcome::Fail(refused) => failure(&refused), | |
| 542 | 572 | }; | |
| 543 | 573 | } | |
| 552 | 582 | body["runId"] = json!(run_id); | |
| 553 | 583 | let reported: Outcome<Value> = g1t_kit::call(&services.work, "report_run", &body).await?; | |
| 554 | 584 | return match reported { | |
| 555 | − | Outcome::Ok(status) => Response::from_json(&json!({ "status": status })), | |
| 585 | + | Outcome::Ok(status) => reply(&json!({ "status": status })), | |
| 556 | 586 | Outcome::Fail(refused) => failure(&refused), | |
| 557 | 587 | }; | |
| 558 | 588 | } | |
| 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 | + | } | |
| 559 | 604 | ("POST", path) if path.starts_with("/checks/") => { | |
| 560 | 605 | let run_id = path.trim_start_matches("/checks/").to_owned(); | |
| 561 | 606 | return report_checks(&mut request, &services, &run_id).await; | |
| 590 | 635 | let Some((route, input)) = rest::resolve(method, &path, &query, body) else { | |
| 591 | 636 | return fail(FailureCode::NotFound, "No such endpoint."); | |
| 592 | 637 | }; | |
| 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), | |
| 595 | 640 | Outcome::Fail(refused) => failure(&refused), | |
| 596 | 641 | } | |
| 597 | 642 | } | |
| 598 | 643 | ||
| 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. | |
| 602 | 646 | fn snake_case_keys(body: Value) -> Value { | |
| 603 | 647 | let Value::Object(fields) = body else { | |
| 604 | 648 | return body; |
| 68 | 68 | let Some(op) = Op::by_name(params["name"].as_str().unwrap_or_default()) else { | |
| 69 | 69 | return Ok(Some(error(id, -32602, "Unknown tool."))); | |
| 70 | 70 | }; | |
| 71 | − | let outcome = op.run(services, viewer, ¶ms["arguments"]).await?; | |
| 71 | + | let outcome = crate::audit::run(op, services, viewer, ¶ms["arguments"]).await?; | |
| 72 | 72 | // A failed operation is a tool result the model can read and | |
| 73 | 73 | // act on, not a protocol error. | |
| 74 | 74 | 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 | + | ), | |
| 76 | 81 | Outcome::Fail(failure) => (failure.message, true), | |
| 77 | 82 | }; | |
| 78 | 83 | result( |
| 83 | 83 | ||
| 84 | 84 | fn oauth_error(error: &str, description: &str) -> Result<Response> { | |
| 85 | 85 | let mut response = | |
| 86 | − | Response::from_json(&json!({ "error": error, "error_description": description }))? | |
| 86 | + | crate::reply(&json!({ "error": error, "error_description": description }))? | |
| 87 | 87 | .with_status(400); | |
| 88 | 88 | response.headers_mut().set("cache-control", "no-store")?; | |
| 89 | 89 | Ok(response) | |
| 144 | 144 | "Give one to five redirect_uris: https addresses, http on localhost, or the application's own scheme.", | |
| 145 | 145 | ); | |
| 146 | 146 | }; | |
| 147 | − | Ok(Response::from_json(&json!({ | |
| 147 | + | Ok(crate::reply(&json!({ | |
| 148 | 148 | "client_id": client_id, | |
| 149 | 149 | "client_name": client_name, | |
| 150 | 150 | "redirect_uris": redirect_uris, | |
| 214 | 214 | Outcome::Ok(tokens) => tokens, | |
| 215 | 215 | Outcome::Fail(failure) => return oauth_error("invalid_grant", &failure.message), | |
| 216 | 216 | }; | |
| 217 | − | let mut response = Response::from_json(&json!({ | |
| 217 | + | let mut response = crate::reply(&json!({ | |
| 218 | 218 | "access_token": tokens.access_token, | |
| 219 | 219 | "token_type": "Bearer", | |
| 220 | 220 | "expires_in": tokens.expires_in, | |
| 234 | 234 | ) -> Result<Option<Response>> { | |
| 235 | 235 | let response = match (method, path) { | |
| 236 | 236 | ("GET", "/.well-known/oauth-authorization-server") => { | |
| 237 | − | Response::from_json(&server_metadata())? | |
| 237 | + | crate::reply(&server_metadata())? | |
| 238 | 238 | } | |
| 239 | 239 | // Asked for with or without the MCP server's path appended. | |
| 240 | 240 | ("GET", path) if path.starts_with("/.well-known/oauth-protected-resource") => { | |
| 241 | − | Response::from_json(&json!({ | |
| 241 | + | crate::reply(&json!({ | |
| 242 | 242 | "resource": MCP_RESOURCE, | |
| 243 | 243 | "authorization_servers": [ISSUER], | |
| 244 | 244 | "bearer_methods_supported": ["header"], |
| 84 | 84 | &[Op::Remember, Op::Recall], | |
| 85 | 85 | ), | |
| 86 | 86 | ( | |
| 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 | + | ( | |
| 87 | 92 | "Actions", | |
| 88 | 93 | "GitHub Actions workflows in .g1t/workflows, their runs, and their jobs' logs.", | |
| 89 | 94 | &[ | |
| 163 | 168 | Op::TakeMessages => "Take new messages", | |
| 164 | 169 | Op::Remember => "Remember something", | |
| 165 | 170 | Op::Recall => "Recall memory", | |
| 171 | + | Op::SearchContext => "Search the context hub", | |
| 172 | + | Op::GetEntity => "Get a catalog entry", | |
| 166 | 173 | Op::ListIssues => "List issues", | |
| 167 | 174 | Op::GetIssue => "Get an issue", | |
| 168 | 175 | Op::CreateIssue => "Create an issue", | |
| 555 | 562 | "info": { | |
| 556 | 563 | "title": "g1t API", | |
| 557 | 564 | "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.", | |
| 559 | 566 | "license": { "name": "MIT", "identifier": "MIT" }, | |
| 560 | 567 | }, | |
| 561 | 568 | "servers": [{ "url": "https://api.g1t.sh" }], | |
| 696 | 703 | ); | |
| 697 | 704 | } | |
| 698 | 705 | ||
| 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 | + | ||
| 699 | 719 | /// Examples never hold anything that reads as a real credential, which | |
| 700 | 720 | /// secret scanners rightly flag in a public repository: they end in `…` | |
| 701 | 721 | /// after the prefix, as `whsec_…` and `g1t_…` do. |
| 26 | 26 | pub integrations: Fetcher, | |
| 27 | 27 | pub webhooks: Fetcher, | |
| 28 | 28 | 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, | |
| 29 | 33 | /// Set for a request made with an agent's token: all it may do. | |
| 30 | 34 | pub scope: Option<AgentScope>, | |
| 31 | 35 | } | |
| 42 | 46 | integrations: env.service("INTEGRATIONS")?, | |
| 43 | 47 | webhooks: env.service("WEBHOOKS")?, | |
| 44 | 48 | actions: env.service("ACTIONS")?, | |
| 49 | + | context: env.service("CONTEXT")?, | |
| 45 | 50 | scope: None, | |
| 51 | + | audit: crate::audit::AuditContext::default(), | |
| 46 | 52 | }) | |
| 47 | 53 | } | |
| 48 | 54 | } | |
| 63 | 69 | TakeMessages, | |
| 64 | 70 | Remember, | |
| 65 | 71 | Recall, | |
| 72 | + | SearchContext, | |
| 73 | + | GetEntity, | |
| 66 | 74 | ListIssues, | |
| 67 | 75 | GetIssue, | |
| 68 | 76 | CreateIssue, | |
| 287 | 295 | } | |
| 288 | 296 | ||
| 289 | 297 | impl Op { | |
| 290 | − | pub const ALL: [Op; 66] = [ | |
| 298 | + | pub const ALL: [Op; 68] = [ | |
| 291 | 299 | Op::Whoami, | |
| 292 | 300 | Op::CreateWorkspace, | |
| 293 | 301 | Op::ListRepos, | |
| 302 | 310 | Op::TakeMessages, | |
| 303 | 311 | Op::Remember, | |
| 304 | 312 | Op::Recall, | |
| 313 | + | Op::SearchContext, | |
| 314 | + | Op::GetEntity, | |
| 305 | 315 | Op::ListIssues, | |
| 306 | 316 | Op::GetIssue, | |
| 307 | 317 | Op::CreateIssue, | |
| 376 | 386 | Op::TakeMessages => "take_messages", | |
| 377 | 387 | Op::Remember => "remember", | |
| 378 | 388 | Op::Recall => "recall", | |
| 389 | + | Op::SearchContext => "search_context", | |
| 390 | + | Op::GetEntity => "get_entity", | |
| 379 | 391 | Op::UpdateRepoSettings => "update_repo_settings", | |
| 380 | 392 | Op::ListIssues => "list_issues", | |
| 381 | 393 | Op::GetIssue => "get_issue", | |
| 463 | 475 | Op::Recall => { | |
| 464 | 476 | "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." | |
| 465 | 477 | } | |
| 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 | + | } | |
| 466 | 484 | Op::TakeMessages => { | |
| 467 | 485 | "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." | |
| 468 | 486 | } | |
| 697 | 715 | }), | |
| 698 | 716 | &["repo"], | |
| 699 | 717 | ), | |
| 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 | + | ), | |
| 700 | 748 | Op::UpdateRepoSettings => object( | |
| 701 | 749 | json!({ | |
| 702 | 750 | "repo": repo_schema(), | |
| 1156 | 1204 | } | |
| 1157 | 1205 | ||
| 1158 | 1206 | /// Whether the operation is about one repository, named by `repo`. | |
| 1159 | − | fn needs_repo(self) -> bool { | |
| 1207 | + | pub(crate) fn needs_repo(self) -> bool { | |
| 1160 | 1208 | !matches!( | |
| 1161 | 1209 | self, | |
| 1162 | 1210 | Op::Whoami | |
| 1163 | 1211 | | Op::CreateWorkspace | |
| 1212 | + | | Op::SearchContext | |
| 1213 | + | | Op::GetEntity | |
| 1164 | 1214 | | Op::ListRepos | |
| 1165 | 1215 | | Op::CreateRepo | |
| 1166 | 1216 | | Op::ListIntegrations | |
| 1401 | 1451 | ) | |
| 1402 | 1452 | .await | |
| 1403 | 1453 | } | |
| 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 | + | } | |
| 1404 | 1498 | Op::Recall => { | |
| 1405 | 1499 | pass( | |
| 1406 | 1500 | work, |
| 52 | 52 | "slug": "syntaqx-labs", | |
| 53 | 53 | "name": "Syntaqx Labs", | |
| 54 | 54 | "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 | |
| 57 | 57 | } | |
| 58 | 58 | }, | |
| 59 | 59 | "list_repos": { | |
| 66 | 66 | "namespace": "syntaqx", | |
| 67 | 67 | "name": "hello", | |
| 68 | 68 | "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, | |
| 73 | 73 | "protected": true, | |
| 74 | − | "createdAt": "2026-09-28T14:11:52.640Z" | |
| 74 | + | "created_at": "2026-09-28T14:11:52.640Z" | |
| 75 | 75 | } | |
| 76 | 76 | ], | |
| 77 | 77 | "notes": "Returns at most 50 repositories, newest first. Forks made for pull requests are left out." | |
| 88 | 88 | "namespace": "syntaqx", | |
| 89 | 89 | "name": "hello-cli", | |
| 90 | 90 | "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, | |
| 95 | 95 | "protected": false, | |
| 96 | − | "createdAt": "2026-10-04T16:05:12.913Z" | |
| 96 | + | "created_at": "2026-10-04T16:05:12.913Z" | |
| 97 | 97 | }, | |
| 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." | |
| 99 | 99 | }, | |
| 100 | 100 | "get_repo": { | |
| 101 | 101 | "response": { | |
| 103 | 103 | "namespace": "syntaqx", | |
| 104 | 104 | "name": "hello", | |
| 105 | 105 | "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, | |
| 110 | 110 | "protected": true, | |
| 111 | − | "createdAt": "2026-09-28T14:11:52.640Z" | |
| 111 | + | "created_at": "2026-09-28T14:11:52.640Z" | |
| 112 | 112 | } | |
| 113 | 113 | }, | |
| 114 | 114 | "update_repo": { | |
| 121 | 121 | "namespace": "syntaqx", | |
| 122 | 122 | "name": "hello", | |
| 123 | 123 | "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, | |
| 128 | 128 | "protected": true, | |
| 129 | − | "createdAt": "2026-09-28T14:11:52.640Z" | |
| 129 | + | "created_at": "2026-09-28T14:11:52.640Z" | |
| 130 | 130 | } | |
| 131 | 131 | }, | |
| 132 | 132 | "get_repo_settings": { | |
| 133 | 133 | "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 | |
| 144 | 144 | } | |
| 145 | 145 | }, | |
| 146 | 146 | "update_repo_settings": { | |
| 150 | 150 | "merge_queue": true | |
| 151 | 151 | }, | |
| 152 | 152 | "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" | |
| 163 | 163 | }, | |
| 164 | 164 | "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)." | |
| 165 | 165 | }, | |
| 173 | 173 | "type": "pull.opened", | |
| 174 | 174 | "source": "work", | |
| 175 | 175 | "time": "2026-10-01T18:20:02.117Z", | |
| 176 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 176 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 177 | 177 | "actor": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 178 | 178 | "data": { | |
| 179 | − | "pullId": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 180 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 179 | + | "pull_id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 180 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 181 | 181 | "number": 14, | |
| 182 | 182 | "issue": 12, | |
| 183 | 183 | "agent": "claude-code" | |
| 188 | 188 | "type": "issue.opened", | |
| 189 | 189 | "source": "work", | |
| 190 | 190 | "time": "2026-10-01T18:04:11.482Z", | |
| 191 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 191 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 192 | 192 | "actor": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 193 | 193 | "data": { | |
| 194 | − | "issueId": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 195 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 194 | + | "issue_id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 195 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 196 | 196 | "number": 12, | |
| 197 | 197 | "title": "Greeting should name the caller" | |
| 198 | 198 | } | |
| 208 | 208 | "response": [ | |
| 209 | 209 | { | |
| 210 | 210 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 211 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 211 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 212 | 212 | "number": 12, | |
| 213 | 213 | "title": "Greeting should name the caller", | |
| 214 | 214 | "body": "Take a name from the first argument; fall back to world.", | |
| 220 | 220 | ], | |
| 221 | 221 | "state": "open", | |
| 222 | 222 | "reason": null, | |
| 223 | − | "resolvedBy": null, | |
| 223 | + | "resolved_by": null, | |
| 224 | 224 | "author": { | |
| 225 | 225 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 226 | 226 | "username": "syntaqx", | |
| 228 | 228 | "verified": false, | |
| 229 | 229 | "workspaces": [] | |
| 230 | 230 | }, | |
| 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, | |
| 236 | 236 | "assignees": [], | |
| 237 | − | "blockedBy": [], | |
| 237 | + | "blocked_by": [], | |
| 238 | 238 | "queued": false, | |
| 239 | 239 | "agent": "claude-code" | |
| 240 | 240 | } | |
| 241 | 241 | ], | |
| 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\"." | |
| 243 | 243 | }, | |
| 244 | 244 | "create_issue": { | |
| 245 | 245 | "request": { | |
| 254 | 254 | }, | |
| 255 | 255 | "response": { | |
| 256 | 256 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 257 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 257 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 258 | 258 | "number": 12, | |
| 259 | 259 | "title": "Greeting should name the caller", | |
| 260 | 260 | "body": "Take a name from the first argument; fall back to world.", | |
| 266 | 266 | ], | |
| 267 | 267 | "state": "open", | |
| 268 | 268 | "reason": null, | |
| 269 | − | "resolvedBy": null, | |
| 269 | + | "resolved_by": null, | |
| 270 | 270 | "author": { | |
| 271 | 271 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 272 | 272 | "username": "syntaqx", | |
| 274 | 274 | "verified": false, | |
| 275 | 275 | "workspaces": [] | |
| 276 | 276 | }, | |
| 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, | |
| 282 | 282 | "assignees": [], | |
| 283 | − | "blockedBy": [], | |
| 283 | + | "blocked_by": [], | |
| 284 | 284 | "queued": false, | |
| 285 | 285 | "agent": null | |
| 286 | 286 | }, | |
| 290 | 290 | "response": { | |
| 291 | 291 | "issue": { | |
| 292 | 292 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 293 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 293 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 294 | 294 | "number": 12, | |
| 295 | 295 | "title": "Greeting should name the caller", | |
| 296 | 296 | "body": "Take a name from the first argument; fall back to world.", | |
| 302 | 302 | ], | |
| 303 | 303 | "state": "closed", | |
| 304 | 304 | "reason": "completed", | |
| 305 | − | "resolvedBy": 14, | |
| 305 | + | "resolved_by": 14, | |
| 306 | 306 | "author": { | |
| 307 | 307 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 308 | 308 | "username": "syntaqx", | |
| 310 | 310 | "verified": false, | |
| 311 | 311 | "workspaces": [] | |
| 312 | 312 | }, | |
| 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, | |
| 318 | 318 | "assignees": [], | |
| 319 | − | "blockedBy": [], | |
| 319 | + | "blocked_by": [], | |
| 320 | 320 | "queued": false, | |
| 321 | 321 | "agent": null | |
| 322 | 322 | }, | |
| 323 | 323 | "pulls": [ | |
| 324 | 324 | { | |
| 325 | 325 | "id": "pr_01m43sjq8tcz5rbm2a7k4d9e6w", | |
| 326 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 326 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 327 | 327 | "number": 13, | |
| 328 | 328 | "issue": 12, | |
| 329 | 329 | "title": "Greeting should name the caller", | |
| 335 | 335 | "namespace": "pulls", | |
| 336 | 336 | "name": "pr_01m43sjq8tcz5rbm2a7k4d9e6w" | |
| 337 | 337 | }, | |
| 338 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 338 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 339 | 339 | "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, | |
| 346 | 346 | "files": [], | |
| 347 | 347 | "assignees": [], | |
| 348 | 348 | "reviewers": [], | |
| 353 | 353 | "verified": false, | |
| 354 | 354 | "workspaces": [] | |
| 355 | 355 | }, | |
| 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" | |
| 358 | 358 | }, | |
| 359 | 359 | { | |
| 360 | 360 | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 361 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 361 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 362 | 362 | "number": 14, | |
| 363 | 363 | "issue": 12, | |
| 364 | 364 | "title": "Greeting should name the caller", | |
| 370 | 370 | "namespace": "pulls", | |
| 371 | 371 | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 372 | 372 | }, | |
| 373 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 373 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 374 | 374 | "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", | |
| 381 | 381 | "files": [ | |
| 382 | 382 | { | |
| 383 | 383 | "path": "src/main.rs", | |
| 396 | 396 | "verified": false, | |
| 397 | 397 | "workspaces": [] | |
| 398 | 398 | }, | |
| 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" | |
| 401 | 401 | } | |
| 402 | 402 | ], | |
| 403 | 403 | "comments": [ | |
| 415 | 415 | "path": null, | |
| 416 | 416 | "line": null, | |
| 417 | 417 | "verdict": null, | |
| 418 | − | "createdAt": "2026-10-01T18:20:02.141Z" | |
| 418 | + | "created_at": "2026-10-01T18:20:02.141Z" | |
| 419 | 419 | } | |
| 420 | 420 | ] | |
| 421 | 421 | }, | |
| 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." | |
| 423 | 423 | }, | |
| 424 | 424 | "update_issue": { | |
| 425 | 425 | "request": { | |
| 433 | 433 | }, | |
| 434 | 434 | "response": { | |
| 435 | 435 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 436 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 436 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 437 | 437 | "number": 12, | |
| 438 | 438 | "title": "Greeting should name the caller", | |
| 439 | 439 | "body": "Take a name from the first argument; fall back to world.", | |
| 446 | 446 | ], | |
| 447 | 447 | "state": "open", | |
| 448 | 448 | "reason": null, | |
| 449 | − | "resolvedBy": null, | |
| 449 | + | "resolved_by": null, | |
| 450 | 450 | "author": { | |
| 451 | 451 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 452 | 452 | "username": "syntaqx", | |
| 454 | 454 | "verified": false, | |
| 455 | 455 | "workspaces": [] | |
| 456 | 456 | }, | |
| 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, | |
| 462 | 462 | "assignees": [ | |
| 463 | 463 | "syntaqx" | |
| 464 | 464 | ], | |
| 465 | − | "blockedBy": [], | |
| 465 | + | "blocked_by": [], | |
| 466 | 466 | "queued": false, | |
| 467 | 467 | "agent": null | |
| 468 | 468 | } | |
| 473 | 473 | }, | |
| 474 | 474 | "response": { | |
| 475 | 475 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 476 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 476 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 477 | 477 | "number": 12, | |
| 478 | 478 | "title": "Greeting should name the caller", | |
| 479 | 479 | "body": "Take a name from the first argument; fall back to world.", | |
| 485 | 485 | ], | |
| 486 | 486 | "state": "closed", | |
| 487 | 487 | "reason": "not_planned", | |
| 488 | − | "resolvedBy": null, | |
| 488 | + | "resolved_by": null, | |
| 489 | 489 | "author": { | |
| 490 | 490 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 491 | 491 | "username": "syntaqx", | |
| 493 | 493 | "verified": false, | |
| 494 | 494 | "workspaces": [] | |
| 495 | 495 | }, | |
| 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, | |
| 501 | 501 | "assignees": [], | |
| 502 | − | "blockedBy": [], | |
| 502 | + | "blocked_by": [], | |
| 503 | 503 | "queued": false, | |
| 504 | 504 | "agent": null | |
| 505 | 505 | } | |
| 507 | 507 | "reopen_issue": { | |
| 508 | 508 | "response": { | |
| 509 | 509 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 510 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 510 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 511 | 511 | "number": 12, | |
| 512 | 512 | "title": "Greeting should name the caller", | |
| 513 | 513 | "body": "Take a name from the first argument; fall back to world.", | |
| 519 | 519 | ], | |
| 520 | 520 | "state": "open", | |
| 521 | 521 | "reason": null, | |
| 522 | − | "resolvedBy": null, | |
| 522 | + | "resolved_by": null, | |
| 523 | 523 | "author": { | |
| 524 | 524 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 525 | 525 | "username": "syntaqx", | |
| 527 | 527 | "verified": false, | |
| 528 | 528 | "workspaces": [] | |
| 529 | 529 | }, | |
| 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, | |
| 535 | 535 | "assignees": [], | |
| 536 | − | "blockedBy": [], | |
| 536 | + | "blocked_by": [], | |
| 537 | 537 | "queued": false, | |
| 538 | 538 | "agent": null | |
| 539 | 539 | } | |
| 544 | 544 | }, | |
| 545 | 545 | "response": { | |
| 546 | 546 | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 547 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 547 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 548 | 548 | "number": 14, | |
| 549 | 549 | "issue": 12, | |
| 550 | 550 | "title": "Greeting should name the caller", | |
| 556 | 556 | "namespace": "pulls", | |
| 557 | 557 | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 558 | 558 | }, | |
| 559 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 559 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 560 | 560 | "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, | |
| 567 | 567 | "files": [], | |
| 568 | 568 | "assignees": [], | |
| 569 | 569 | "reviewers": [], | |
| 574 | 574 | "verified": false, | |
| 575 | 575 | "workspaces": [] | |
| 576 | 576 | }, | |
| 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" | |
| 579 | 579 | }, | |
| 580 | 580 | "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." | |
| 581 | 581 | }, | |
| 602 | 602 | "path": null, | |
| 603 | 603 | "line": null, | |
| 604 | 604 | "verdict": null, | |
| 605 | − | "createdAt": "2026-10-01T18:12:30.551Z" | |
| 605 | + | "created_at": "2026-10-01T18:12:30.551Z" | |
| 606 | 606 | } | |
| 607 | 607 | }, | |
| 608 | 608 | "list_labels": { | |
| 621 | 621 | "brief": "Greet people by name, from the command line and the web page." | |
| 622 | 622 | }, | |
| 623 | 623 | "response": { | |
| 624 | − | "planId": "pln_01m43s9c4e8g2j6m0q4t8x2b6d" | |
| 624 | + | "plan_id": "pln_01m43s9c4e8g2j6m0q4t8x2b6d" | |
| 625 | 625 | } | |
| 626 | 626 | }, | |
| 627 | 627 | "get_plan": { | |
| 630 | 630 | }, | |
| 631 | 631 | "response": { | |
| 632 | 632 | "id": "pln_01m43s9c4e8g2j6m0q4t8x2b6d", | |
| 633 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 633 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 634 | 634 | "brief": "Greet people by name, from the command line and the web page.", | |
| 635 | 635 | "status": "ready", | |
| 636 | 636 | "summary": "Name parsing first, then the two places that print the greeting.", | |
| 647 | 647 | "files": [ | |
| 648 | 648 | "src/greet.rs" | |
| 649 | 649 | ], | |
| 650 | − | "dependsOn": [], | |
| 650 | + | "depends_on": [], | |
| 651 | 651 | "number": null | |
| 652 | 652 | }, | |
| 653 | 653 | { | |
| 662 | 662 | "files": [ | |
| 663 | 663 | "src/web.rs" | |
| 664 | 664 | ], | |
| 665 | − | "dependsOn": [ | |
| 665 | + | "depends_on": [ | |
| 666 | 666 | 1 | |
| 667 | 667 | ], | |
| 668 | 668 | "number": null | |
| 676 | 676 | "verified": false, | |
| 677 | 677 | "workspaces": [] | |
| 678 | 678 | }, | |
| 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", | |
| 681 | 681 | "progress": [], | |
| 682 | 682 | "exchanges": [] | |
| 683 | 683 | }, | |
| 692 | 692 | }, | |
| 693 | 693 | "response": { | |
| 694 | 694 | "id": "pln_01m43s9c4e8g2j6m0q4t8x2b6d", | |
| 695 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 695 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 696 | 696 | "brief": "Greet people by name, from the command line and the web page.", | |
| 697 | 697 | "status": "applied", | |
| 698 | 698 | "summary": "Name parsing first, then the two places that print the greeting.", | |
| 709 | 709 | "files": [ | |
| 710 | 710 | "src/greet.rs" | |
| 711 | 711 | ], | |
| 712 | − | "dependsOn": [], | |
| 712 | + | "depends_on": [], | |
| 713 | 713 | "number": 12 | |
| 714 | 714 | }, | |
| 715 | 715 | { | |
| 724 | 724 | "files": [ | |
| 725 | 725 | "src/web.rs" | |
| 726 | 726 | ], | |
| 727 | − | "dependsOn": [ | |
| 727 | + | "depends_on": [ | |
| 728 | 728 | 1 | |
| 729 | 729 | ], | |
| 730 | 730 | "number": 13 | |
| 738 | 738 | "verified": false, | |
| 739 | 739 | "workspaces": [] | |
| 740 | 740 | }, | |
| 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", | |
| 743 | 743 | "progress": [], | |
| 744 | 744 | "exchanges": [] | |
| 745 | 745 | }, | |
| 752 | 752 | "response": [ | |
| 753 | 753 | { | |
| 754 | 754 | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 755 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 755 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 756 | 756 | "number": 14, | |
| 757 | 757 | "issue": 12, | |
| 758 | 758 | "title": "Greeting should name the caller", | |
| 764 | 764 | "namespace": "pulls", | |
| 765 | 765 | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 766 | 766 | }, | |
| 767 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 767 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 768 | 768 | "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", | |
| 775 | 775 | "files": [ | |
| 776 | 776 | { | |
| 777 | 777 | "path": "src/main.rs", | |
| 790 | 790 | "verified": false, | |
| 791 | 791 | "workspaces": [] | |
| 792 | 792 | }, | |
| 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" | |
| 795 | 795 | } | |
| 796 | 796 | ], | |
| 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." | |
| 798 | 798 | }, | |
| 799 | 799 | "create_pull_request": { | |
| 800 | 800 | "request": { | |
| 804 | 804 | "response": { | |
| 805 | 805 | "pull": { | |
| 806 | 806 | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 807 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 807 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 808 | 808 | "number": 14, | |
| 809 | 809 | "issue": 12, | |
| 810 | 810 | "title": "Greeting should name the caller", | |
| 816 | 816 | "namespace": "pulls", | |
| 817 | 817 | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 818 | 818 | }, | |
| 819 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 819 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 820 | 820 | "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, | |
| 827 | 827 | "files": [], | |
| 828 | 828 | "assignees": [], | |
| 829 | 829 | "reviewers": [], | |
| 834 | 834 | "verified": false, | |
| 835 | 835 | "workspaces": [] | |
| 836 | 836 | }, | |
| 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" | |
| 839 | 839 | }, | |
| 840 | 840 | "git": { | |
| 841 | 841 | "remote": "https://g1t.sh/pulls/pr_01m43smh3vexsr5pmp60qwv0vs.git", | |
| 849 | 849 | "response": { | |
| 850 | 850 | "pull": { | |
| 851 | 851 | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 852 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 852 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 853 | 853 | "number": 14, | |
| 854 | 854 | "issue": 12, | |
| 855 | 855 | "title": "Greeting should name the caller", | |
| 861 | 861 | "namespace": "pulls", | |
| 862 | 862 | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 863 | 863 | }, | |
| 864 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 864 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 865 | 865 | "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", | |
| 872 | 872 | "files": [ | |
| 873 | 873 | { | |
| 874 | 874 | "path": "src/main.rs", | |
| 887 | 887 | "verified": false, | |
| 888 | 888 | "workspaces": [] | |
| 889 | 889 | }, | |
| 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" | |
| 892 | 892 | }, | |
| 893 | 893 | "issue": { | |
| 894 | 894 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 895 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 895 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 896 | 896 | "number": 12, | |
| 897 | 897 | "title": "Greeting should name the caller", | |
| 898 | 898 | "body": "Take a name from the first argument; fall back to world.", | |
| 904 | 904 | ], | |
| 905 | 905 | "state": "open", | |
| 906 | 906 | "reason": null, | |
| 907 | − | "resolvedBy": null, | |
| 907 | + | "resolved_by": null, | |
| 908 | 908 | "author": { | |
| 909 | 909 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 910 | 910 | "username": "syntaqx", | |
| 912 | 912 | "verified": false, | |
| 913 | 913 | "workspaces": [] | |
| 914 | 914 | }, | |
| 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, | |
| 920 | 920 | "assignees": [], | |
| 921 | − | "blockedBy": [], | |
| 921 | + | "blocked_by": [], | |
| 922 | 922 | "queued": false, | |
| 923 | 923 | "agent": "claude-code" | |
| 924 | 924 | }, | |
| 937 | 937 | "path": null, | |
| 938 | 938 | "line": null, | |
| 939 | 939 | "verdict": null, | |
| 940 | − | "createdAt": "2026-10-01T18:33:10.420Z" | |
| 940 | + | "created_at": "2026-10-01T18:33:10.420Z" | |
| 941 | 941 | } | |
| 942 | 942 | ], | |
| 943 | 943 | "checks": { | |
| 944 | 944 | "id": "chk_01m43sw8e2g6j0m4q8t2x6a0c4", | |
| 945 | − | "headCommit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 945 | + | "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 946 | 946 | "status": "passed", | |
| 947 | 947 | "results": [ | |
| 948 | 948 | { | |
| 949 | 949 | "command": "cargo test", | |
| 950 | 950 | "passed": true, | |
| 951 | − | "exitCode": 0, | |
| 951 | + | "exit_code": 0, | |
| 952 | 952 | "output": "test result: ok. 3 passed; 0 failed", | |
| 953 | − | "durationMs": 18234 | |
| 953 | + | "duration_ms": 18234 | |
| 954 | 954 | } | |
| 955 | 955 | ], | |
| 956 | 956 | "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" | |
| 959 | 959 | }, | |
| 960 | 960 | "overlaps": [], | |
| 961 | 961 | "behind": false, | |
| 968 | 968 | "id": "msg_01m43tx5egeh4t3f9zcnjenqvz", | |
| 969 | 969 | "author": "syntaqx", | |
| 970 | 970 | "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", | |
| 973 | 973 | "kind": "message", | |
| 974 | − | "fromNumber": null, | |
| 975 | − | "toNumber": 14, | |
| 974 | + | "from_number": null, | |
| 975 | + | "to_number": 14, | |
| 976 | 976 | "answer": null, | |
| 977 | 977 | "declined": false | |
| 978 | 978 | } | |
| 1034 | 1034 | }, | |
| 1035 | 1035 | "response": { | |
| 1036 | 1036 | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 1037 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 1037 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 1038 | 1038 | "number": 14, | |
| 1039 | 1039 | "issue": 12, | |
| 1040 | 1040 | "title": "Greeting should name the caller", | |
| 1046 | 1046 | "namespace": "pulls", | |
| 1047 | 1047 | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 1048 | 1048 | }, | |
| 1049 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 1049 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 1050 | 1050 | "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, | |
| 1057 | 1057 | "files": [ | |
| 1058 | 1058 | { | |
| 1059 | 1059 | "path": "src/main.rs", | |
| 1070 | 1070 | "verified": false, | |
| 1071 | 1071 | "workspaces": [] | |
| 1072 | 1072 | }, | |
| 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" | |
| 1075 | 1075 | } | |
| 1076 | 1076 | }, | |
| 1077 | 1077 | "review_pull_request": { | |
| 1098 | 1098 | "path": null, | |
| 1099 | 1099 | "line": null, | |
| 1100 | 1100 | "verdict": "request_changes", | |
| 1101 | − | "createdAt": "2026-10-01T18:40:05.019Z" | |
| 1101 | + | "created_at": "2026-10-01T18:40:05.019Z" | |
| 1102 | 1102 | } | |
| 1103 | 1103 | }, | |
| 1104 | 1104 | "merge_pull_request": { | |
| 1107 | 1107 | }, | |
| 1108 | 1108 | "response": { | |
| 1109 | 1109 | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 1110 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 1110 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 1111 | 1111 | "number": 14, | |
| 1112 | 1112 | "issue": 12, | |
| 1113 | 1113 | "title": "Greeting should name the caller", | |
| 1119 | 1119 | "namespace": "pulls", | |
| 1120 | 1120 | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 1121 | 1121 | }, | |
| 1122 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 1122 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 1123 | 1123 | "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", | |
| 1130 | 1130 | "files": [ | |
| 1131 | 1131 | { | |
| 1132 | 1132 | "path": "src/main.rs", | |
| 1145 | 1145 | "verified": false, | |
| 1146 | 1146 | "workspaces": [] | |
| 1147 | 1147 | }, | |
| 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" | |
| 1150 | 1150 | }, | |
| 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." | |
| 1152 | 1152 | }, | |
| 1153 | 1153 | "close_pull_request": { | |
| 1154 | 1154 | "response": { | |
| 1155 | 1155 | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 1156 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 1156 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 1157 | 1157 | "number": 14, | |
| 1158 | 1158 | "issue": 12, | |
| 1159 | 1159 | "title": "Greeting should name the caller", | |
| 1165 | 1165 | "namespace": "pulls", | |
| 1166 | 1166 | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 1167 | 1167 | }, | |
| 1168 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 1168 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 1169 | 1169 | "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, | |
| 1176 | 1176 | "files": [ | |
| 1177 | 1177 | { | |
| 1178 | 1178 | "path": "src/main.rs", | |
| 1189 | 1189 | "verified": false, | |
| 1190 | 1190 | "workspaces": [] | |
| 1191 | 1191 | }, | |
| 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" | |
| 1194 | 1194 | } | |
| 1195 | 1195 | }, | |
| 1196 | 1196 | "get_merge_queue": { | |
| 1204 | 1204 | "agent": "claude-code", | |
| 1205 | 1205 | "state": "testing", | |
| 1206 | 1206 | "ahead": [], | |
| 1207 | − | "baseCommit": "3a7e9c1b5d2f4a6c8e0b2d4f6a8c0e2b4d6f8a1c", | |
| 1208 | − | "combinedCommit": null, | |
| 1207 | + | "base_commit": "3a7e9c1b5d2f4a6c8e0b2d4f6a8c0e2b4d6f8a1c", | |
| 1208 | + | "combined_commit": null, | |
| 1209 | 1209 | "error": null, | |
| 1210 | 1210 | "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 | |
| 1214 | 1214 | } | |
| 1215 | 1215 | ], | |
| 1216 | 1216 | "recent": [ | |
| 1221 | 1221 | "agent": "g1t-agent", | |
| 1222 | 1222 | "state": "landed", | |
| 1223 | 1223 | "ahead": [], | |
| 1224 | − | "baseCommit": "4b1d7e9c2a5f8e3d6c0b9a7e5d3c1b8a6f4e2d0c", | |
| 1225 | − | "combinedCommit": "c3e8a1f6d4b2097e5c3a1f8d6b4e2c0a9f7d5b3e", | |
| 1224 | + | "base_commit": "4b1d7e9c2a5f8e3d6c0b9a7e5d3c1b8a6f4e2d0c", | |
| 1225 | + | "combined_commit": "c3e8a1f6d4b2097e5c3a1f8d6b4e2c0a9f7d5b3e", | |
| 1226 | 1226 | "error": null, | |
| 1227 | 1227 | "results": [ | |
| 1228 | 1228 | { | |
| 1229 | 1229 | "command": "cargo test", | |
| 1230 | 1230 | "passed": true, | |
| 1231 | − | "exitCode": 0, | |
| 1231 | + | "exit_code": 0, | |
| 1232 | 1232 | "output": "test result: ok. 12 passed; 0 failed\n", | |
| 1233 | − | "durationMs": 48211 | |
| 1233 | + | "duration_ms": 48211 | |
| 1234 | 1234 | } | |
| 1235 | 1235 | ], | |
| 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" | |
| 1239 | 1239 | } | |
| 1240 | 1240 | ] | |
| 1241 | 1241 | }, | |
| 1249 | 1249 | "id": "msg_01m43tx5egeh4t3f9zcnjenqvz", | |
| 1250 | 1250 | "author": "syntaqx", | |
| 1251 | 1251 | "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, | |
| 1254 | 1254 | "kind": "message", | |
| 1255 | − | "fromNumber": null, | |
| 1256 | − | "toNumber": 14, | |
| 1255 | + | "from_number": null, | |
| 1256 | + | "to_number": 14, | |
| 1257 | 1257 | "answer": null, | |
| 1258 | 1258 | "declined": false | |
| 1259 | 1259 | }, | |
| 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." | |
| 1261 | 1261 | }, | |
| 1262 | 1262 | "answer_message": { | |
| 1263 | 1263 | "params": { | |
| 1270 | 1270 | "id": "msg_01m43t66tde8hs2vpxhh3v8tqw", | |
| 1271 | 1271 | "author": "g1t-agent", | |
| 1272 | 1272 | "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", | |
| 1275 | 1275 | "kind": "question", | |
| 1276 | − | "fromNumber": 16, | |
| 1277 | − | "toNumber": 14, | |
| 1276 | + | "from_number": 16, | |
| 1277 | + | "to_number": 14, | |
| 1278 | 1278 | "answer": "No. greet() keeps its name; only greet_named() is added.", | |
| 1279 | 1279 | "declined": false | |
| 1280 | 1280 | }, | |
| 1286 | 1286 | "id": "msg_01m43tx5egeh4t3f9zcnjenqvz", | |
| 1287 | 1287 | "author": "syntaqx", | |
| 1288 | 1288 | "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", | |
| 1291 | 1291 | "kind": "message", | |
| 1292 | − | "fromNumber": null, | |
| 1293 | − | "toNumber": 14, | |
| 1292 | + | "from_number": null, | |
| 1293 | + | "to_number": 14, | |
| 1294 | 1294 | "answer": null, | |
| 1295 | 1295 | "declined": false | |
| 1296 | 1296 | } | |
| 1366 | 1366 | "default": false | |
| 1367 | 1367 | } | |
| 1368 | 1368 | }, | |
| 1369 | − | "lastRun": { | |
| 1369 | + | "last_run": { | |
| 1370 | 1370 | "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 1371 | − | "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 1371 | + | "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 1372 | 1372 | "path": ".g1t/workflows/ci.yml", | |
| 1373 | 1373 | "name": "CI", | |
| 1374 | 1374 | "title": "Greeting should name the caller", | |
| 1382 | 1382 | "conclusion": "success", | |
| 1383 | 1383 | "error": null, | |
| 1384 | 1384 | "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" | |
| 1388 | 1388 | } | |
| 1389 | 1389 | }, | |
| 1390 | 1390 | { | |
| 1398 | 1398 | "error": null, | |
| 1399 | 1399 | "notes": [], | |
| 1400 | 1400 | "dispatch": null, | |
| 1401 | − | "lastRun": null | |
| 1401 | + | "last_run": null | |
| 1402 | 1402 | } | |
| 1403 | 1403 | ], | |
| 1404 | 1404 | "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/)." | |
| 1412 | 1412 | "response": [ | |
| 1413 | 1413 | { | |
| 1414 | 1414 | "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 1415 | − | "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 1415 | + | "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 1416 | 1416 | "path": ".g1t/workflows/ci.yml", | |
| 1417 | 1417 | "name": "CI", | |
| 1418 | 1418 | "title": "Greeting should name the caller", | |
| 1426 | 1426 | "conclusion": "success", | |
| 1427 | 1427 | "error": null, | |
| 1428 | 1428 | "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" | |
| 1432 | 1432 | } | |
| 1433 | 1433 | ], | |
| 1434 | 1434 | "notes": "A run's `status` is `queued`, `pending` (waiting for its concurrency group), `in_progress` or `completed`; `conclusion` is set once it completes." | |
| 1444 | 1444 | "response": [ | |
| 1445 | 1445 | { | |
| 1446 | 1446 | "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 1447 | − | "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 1447 | + | "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 1448 | 1448 | "path": ".g1t/workflows/ci.yml", | |
| 1449 | 1449 | "name": "CI", | |
| 1450 | 1450 | "title": "Greeting should name the caller", | |
| 1458 | 1458 | "conclusion": "success", | |
| 1459 | 1459 | "error": null, | |
| 1460 | 1460 | "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" | |
| 1464 | 1464 | } | |
| 1465 | 1465 | ] | |
| 1466 | 1466 | }, | |
| 1471 | 1471 | "response": { | |
| 1472 | 1472 | "run": { | |
| 1473 | 1473 | "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 1474 | − | "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 1474 | + | "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 1475 | 1475 | "path": ".g1t/workflows/ci.yml", | |
| 1476 | 1476 | "name": "CI", | |
| 1477 | 1477 | "title": "Greeting should name the caller", | |
| 1485 | 1485 | "conclusion": "failure", | |
| 1486 | 1486 | "error": null, | |
| 1487 | 1487 | "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" | |
| 1491 | 1491 | }, | |
| 1492 | 1492 | "jobs": [ | |
| 1493 | 1493 | { | |
| 1494 | 1494 | "id": "job_01kpx7b3d0e4f8g2h6j0k4m8ns", | |
| 1495 | − | "runId": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 1495 | + | "run_id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 1496 | 1496 | "key": "test", | |
| 1497 | 1497 | "name": "test", | |
| 1498 | 1498 | "needs": [], | |
| 1504 | 1504 | "name": "Run actions/checkout@v4", | |
| 1505 | 1505 | "status": "completed", | |
| 1506 | 1506 | "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" | |
| 1509 | 1509 | }, | |
| 1510 | 1510 | { | |
| 1511 | 1511 | "number": 2, | |
| 1512 | 1512 | "name": "Run cargo test", | |
| 1513 | 1513 | "status": "completed", | |
| 1514 | 1514 | "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" | |
| 1517 | 1517 | } | |
| 1518 | 1518 | ], | |
| 1519 | 1519 | "annotations": [ | |
| 1526 | 1526 | } | |
| 1527 | 1527 | ], | |
| 1528 | 1528 | "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" | |
| 1531 | 1531 | }, | |
| 1532 | 1532 | { | |
| 1533 | 1533 | "id": "job_01kpx7b3d0e4f8g2h6j0k4m8nt", | |
| 1534 | − | "runId": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 1534 | + | "run_id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 1535 | 1535 | "key": "deploy", | |
| 1536 | 1536 | "name": "deploy", | |
| 1537 | 1537 | "needs": [ | |
| 1542 | 1542 | "steps": [], | |
| 1543 | 1543 | "annotations": [], | |
| 1544 | 1544 | "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" | |
| 1547 | 1547 | } | |
| 1548 | 1548 | ], | |
| 1549 | 1549 | "notes": [ | |
| 1591 | 1591 | }, | |
| 1592 | 1592 | "response": { | |
| 1593 | 1593 | "id": "run_01kpx8d4e7f0g3h6j9k2m5n8pq", | |
| 1594 | − | "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 1594 | + | "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 1595 | 1595 | "path": ".g1t/workflows/ci.yml", | |
| 1596 | 1596 | "name": "CI", | |
| 1597 | 1597 | "title": "CI run by syntaqx", | |
| 1605 | 1605 | "conclusion": null, | |
| 1606 | 1606 | "error": null, | |
| 1607 | 1607 | "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 | |
| 1611 | 1611 | } | |
| 1612 | 1612 | }, | |
| 1613 | 1613 | "cancel_workflow_run": { | |
| 1616 | 1616 | }, | |
| 1617 | 1617 | "response": { | |
| 1618 | 1618 | "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 1619 | − | "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 1619 | + | "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 1620 | 1620 | "path": ".g1t/workflows/ci.yml", | |
| 1621 | 1621 | "name": "CI", | |
| 1622 | 1622 | "title": "Greeting should name the caller", | |
| 1630 | 1630 | "conclusion": "cancelled", | |
| 1631 | 1631 | "error": null, | |
| 1632 | 1632 | "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" | |
| 1636 | 1636 | } | |
| 1637 | 1637 | }, | |
| 1638 | 1638 | "rerun_workflow_run": { | |
| 1644 | 1644 | }, | |
| 1645 | 1645 | "response": { | |
| 1646 | 1646 | "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 1647 | − | "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 1647 | + | "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 1648 | 1648 | "path": ".g1t/workflows/ci.yml", | |
| 1649 | 1649 | "name": "CI", | |
| 1650 | 1650 | "title": "Greeting should name the caller", | |
| 1658 | 1658 | "conclusion": null, | |
| 1659 | 1659 | "error": null, | |
| 1660 | 1660 | "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 | |
| 1664 | 1664 | }, | |
| 1665 | 1665 | "notes": "The run keeps its id; `attempt` goes up by one." | |
| 1666 | 1666 | }, | |
| 1670 | 1670 | }, | |
| 1671 | 1671 | "response": { | |
| 1672 | 1672 | "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 1673 | − | "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 1673 | + | "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 1674 | 1674 | "path": ".g1t/workflows/ci.yml", | |
| 1675 | 1675 | "name": "CI", | |
| 1676 | 1676 | "title": "Greeting should name the caller", | |
| 1684 | 1684 | "conclusion": null, | |
| 1685 | 1685 | "error": null, | |
| 1686 | 1686 | "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 | |
| 1690 | 1690 | } | |
| 1691 | 1691 | }, | |
| 1692 | 1692 | "update_workflow": { | |
| 1707 | 1707 | "error": null, | |
| 1708 | 1708 | "notes": [], | |
| 1709 | 1709 | "dispatch": null, | |
| 1710 | − | "lastRun": null | |
| 1710 | + | "last_run": null | |
| 1711 | 1711 | } | |
| 1712 | 1712 | }, | |
| 1713 | 1713 | "enable_workflow": { | |
| 1725 | 1725 | "error": null, | |
| 1726 | 1726 | "notes": [], | |
| 1727 | 1727 | "dispatch": null, | |
| 1728 | − | "lastRun": null | |
| 1728 | + | "last_run": null | |
| 1729 | 1729 | } | |
| 1730 | 1730 | }, | |
| 1731 | 1731 | "disable_workflow": { | |
| 1743 | 1743 | "error": null, | |
| 1744 | 1744 | "notes": [], | |
| 1745 | 1745 | "dispatch": null, | |
| 1746 | − | "lastRun": null | |
| 1746 | + | "last_run": null | |
| 1747 | 1747 | } | |
| 1748 | 1748 | }, | |
| 1749 | 1749 | "list_actions_secrets": { | |
| 1754 | 1754 | "kind": "secret", | |
| 1755 | 1755 | "value": null, | |
| 1756 | 1756 | "scope": "workspace", | |
| 1757 | − | "updatedAt": "2026-10-01T09:12:44.020Z", | |
| 1758 | − | "availableTo": [ | |
| 1757 | + | "updated_at": "2026-10-01T09:12:44.020Z", | |
| 1758 | + | "available_to": [ | |
| 1759 | 1759 | "workflows" | |
| 1760 | 1760 | ], | |
| 1761 | 1761 | "environments": [], | |
| 1762 | 1762 | "projects": [], | |
| 1763 | 1763 | "note": null, | |
| 1764 | − | "updatedBy": "syntaqx" | |
| 1764 | + | "updated_by": "syntaqx" | |
| 1765 | 1765 | }, | |
| 1766 | 1766 | { | |
| 1767 | 1767 | "id": "set_01kpv9q4r7s0t3v6w9x2y5z8ac", | |
| 1769 | 1769 | "kind": "secret", | |
| 1770 | 1770 | "value": null, | |
| 1771 | 1771 | "scope": "project", | |
| 1772 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 1773 | − | "availableTo": [ | |
| 1772 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 1773 | + | "available_to": [ | |
| 1774 | 1774 | "workflows", | |
| 1775 | 1775 | "deployments" | |
| 1776 | 1776 | ], | |
| 1779 | 1779 | ], | |
| 1780 | 1780 | "projects": [], | |
| 1781 | 1781 | "note": "Rotate in the Stripe dashboard.", | |
| 1782 | − | "updatedBy": "syntaqx" | |
| 1782 | + | "updated_by": "syntaqx" | |
| 1783 | 1783 | } | |
| 1784 | 1784 | ], | |
| 1785 | 1785 | "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." | |
| 1795 | 1795 | "kind": "secret", | |
| 1796 | 1796 | "value": null, | |
| 1797 | 1797 | "scope": "workspace", | |
| 1798 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 1799 | − | "availableTo": [ | |
| 1798 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 1799 | + | "available_to": [ | |
| 1800 | 1800 | "workflows" | |
| 1801 | 1801 | ], | |
| 1802 | 1802 | "environments": [], | |
| 1803 | 1803 | "projects": [], | |
| 1804 | 1804 | "note": null, | |
| 1805 | − | "updatedBy": "syntaqx" | |
| 1805 | + | "updated_by": "syntaqx" | |
| 1806 | 1806 | } | |
| 1807 | 1807 | ], | |
| 1808 | 1808 | "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." | |
| 1827 | 1827 | "kind": "secret", | |
| 1828 | 1828 | "value": null, | |
| 1829 | 1829 | "scope": "project", | |
| 1830 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 1831 | − | "availableTo": [ | |
| 1830 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 1831 | + | "available_to": [ | |
| 1832 | 1832 | "workflows", | |
| 1833 | 1833 | "deployments" | |
| 1834 | 1834 | ], | |
| 1837 | 1837 | ], | |
| 1838 | 1838 | "projects": [], | |
| 1839 | 1839 | "note": null, | |
| 1840 | − | "updatedBy": "syntaqx" | |
| 1840 | + | "updated_by": "syntaqx" | |
| 1841 | 1841 | }, | |
| 1842 | 1842 | "notes": "Keys are uppercased. See [secrets and variables](/guides/secrets-and-variables/)." | |
| 1843 | 1843 | }, | |
| 1858 | 1858 | "kind": "secret", | |
| 1859 | 1859 | "value": null, | |
| 1860 | 1860 | "scope": "workspace", | |
| 1861 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 1862 | − | "availableTo": [ | |
| 1861 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 1862 | + | "available_to": [ | |
| 1863 | 1863 | "workflows" | |
| 1864 | 1864 | ], | |
| 1865 | 1865 | "environments": [], | |
| 1867 | 1867 | "hello" | |
| 1868 | 1868 | ], | |
| 1869 | 1869 | "note": null, | |
| 1870 | − | "updatedBy": "syntaqx" | |
| 1870 | + | "updated_by": "syntaqx" | |
| 1871 | 1871 | } | |
| 1872 | 1872 | }, | |
| 1873 | 1873 | "delete_actions_secret": { | |
| 1891 | 1891 | "kind": "variable", | |
| 1892 | 1892 | "value": "20", | |
| 1893 | 1893 | "scope": "project", | |
| 1894 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 1895 | − | "availableTo": [ | |
| 1894 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 1895 | + | "available_to": [ | |
| 1896 | 1896 | "workflows", | |
| 1897 | 1897 | "deployments" | |
| 1898 | 1898 | ], | |
| 1899 | 1899 | "environments": [], | |
| 1900 | 1900 | "projects": [], | |
| 1901 | 1901 | "note": null, | |
| 1902 | − | "updatedBy": "syntaqx" | |
| 1902 | + | "updated_by": "syntaqx" | |
| 1903 | 1903 | } | |
| 1904 | 1904 | ], | |
| 1905 | 1905 | "notes": "A repository's list includes the workspace's rows that reach it, with `scope` set to `workspace`. Empty `environments` means every environment." | |
| 1915 | 1915 | "kind": "variable", | |
| 1916 | 1916 | "value": "20", | |
| 1917 | 1917 | "scope": "workspace", | |
| 1918 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 1919 | − | "availableTo": [ | |
| 1918 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 1919 | + | "available_to": [ | |
| 1920 | 1920 | "workflows", | |
| 1921 | 1921 | "deployments" | |
| 1922 | 1922 | ], | |
| 1923 | 1923 | "environments": [], | |
| 1924 | 1924 | "projects": [], | |
| 1925 | 1925 | "note": null, | |
| 1926 | − | "updatedBy": "syntaqx" | |
| 1926 | + | "updated_by": "syntaqx" | |
| 1927 | 1927 | } | |
| 1928 | 1928 | ] | |
| 1929 | 1929 | }, | |
| 1938 | 1938 | "kind": "variable", | |
| 1939 | 1939 | "value": "20", | |
| 1940 | 1940 | "scope": "project", | |
| 1941 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 1942 | − | "availableTo": [ | |
| 1941 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 1942 | + | "available_to": [ | |
| 1943 | 1943 | "workflows", | |
| 1944 | 1944 | "deployments" | |
| 1945 | 1945 | ], | |
| 1946 | 1946 | "environments": [], | |
| 1947 | 1947 | "projects": [], | |
| 1948 | 1948 | "note": null, | |
| 1949 | − | "updatedBy": "syntaqx" | |
| 1949 | + | "updated_by": "syntaqx" | |
| 1950 | 1950 | }, | |
| 1951 | 1951 | "notes": "GitHub's clients send the key as `name`; that is accepted too." | |
| 1952 | 1952 | }, | |
| 1964 | 1964 | "kind": "variable", | |
| 1965 | 1965 | "value": "20", | |
| 1966 | 1966 | "scope": "workspace", | |
| 1967 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 1968 | − | "availableTo": [ | |
| 1967 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 1968 | + | "available_to": [ | |
| 1969 | 1969 | "workflows", | |
| 1970 | 1970 | "deployments" | |
| 1971 | 1971 | ], | |
| 1972 | 1972 | "environments": [], | |
| 1973 | 1973 | "projects": [], | |
| 1974 | 1974 | "note": null, | |
| 1975 | − | "updatedBy": "syntaqx" | |
| 1975 | + | "updated_by": "syntaqx" | |
| 1976 | 1976 | } | |
| 1977 | 1977 | }, | |
| 1978 | 1978 | "update_actions_variable": { | |
| 1988 | 1988 | "kind": "variable", | |
| 1989 | 1989 | "value": "22", | |
| 1990 | 1990 | "scope": "project", | |
| 1991 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 1992 | − | "availableTo": [ | |
| 1991 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 1992 | + | "available_to": [ | |
| 1993 | 1993 | "workflows", | |
| 1994 | 1994 | "deployments" | |
| 1995 | 1995 | ], | |
| 1996 | 1996 | "environments": [], | |
| 1997 | 1997 | "projects": [], | |
| 1998 | 1998 | "note": null, | |
| 1999 | − | "updatedBy": "syntaqx" | |
| 1999 | + | "updated_by": "syntaqx" | |
| 2000 | 2000 | } | |
| 2001 | 2001 | }, | |
| 2002 | 2002 | "update_actions_variable_for_workspace": { | |
| 2013 | 2013 | "kind": "variable", | |
| 2014 | 2014 | "value": "22", | |
| 2015 | 2015 | "scope": "workspace", | |
| 2016 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 2017 | − | "availableTo": [ | |
| 2016 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 2017 | + | "available_to": [ | |
| 2018 | 2018 | "workflows", | |
| 2019 | 2019 | "deployments" | |
| 2020 | 2020 | ], | |
| 2021 | 2021 | "environments": [], | |
| 2022 | 2022 | "projects": [], | |
| 2023 | 2023 | "note": null, | |
| 2024 | − | "updatedBy": "syntaqx" | |
| 2024 | + | "updated_by": "syntaqx" | |
| 2025 | 2025 | } | |
| 2026 | 2026 | }, | |
| 2027 | 2027 | "delete_actions_variable": { | |
| 2050 | 2050 | "pull.merged" | |
| 2051 | 2051 | ], | |
| 2052 | 2052 | "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" | |
| 2058 | 2058 | } | |
| 2059 | 2059 | ] | |
| 2060 | 2060 | }, | |
| 2074 | 2074 | "pull.merged" | |
| 2075 | 2075 | ], | |
| 2076 | 2076 | "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" | |
| 2082 | 2082 | } | |
| 2083 | 2083 | ] | |
| 2084 | 2084 | }, | |
| 2102 | 2102 | "pull.merged" | |
| 2103 | 2103 | ], | |
| 2104 | 2104 | "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" | |
| 2110 | 2110 | }, | |
| 2111 | 2111 | "secret": "whsec_…" | |
| 2112 | 2112 | }, | |
| 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/)." | |
| 2114 | 2114 | }, | |
| 2115 | 2115 | "create_webhook_for_workspace": { | |
| 2116 | 2116 | "params": { | |
| 2130 | 2130 | "*" | |
| 2131 | 2131 | ], | |
| 2132 | 2132 | "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" | |
| 2138 | 2138 | }, | |
| 2139 | 2139 | "secret": "whsec_…" | |
| 2140 | 2140 | }, | |
| 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/)." | |
| 2142 | 2142 | }, | |
| 2143 | 2143 | "update_webhook": { | |
| 2144 | 2144 | "params": { | |
| 2158 | 2158 | "pull.merged" | |
| 2159 | 2159 | ], | |
| 2160 | 2160 | "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" | |
| 2166 | 2166 | } | |
| 2167 | 2167 | }, | |
| 2168 | 2168 | "update_webhook_for_workspace": { | |
| 2184 | 2184 | "pull.merged" | |
| 2185 | 2185 | ], | |
| 2186 | 2186 | "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" | |
| 2192 | 2192 | } | |
| 2193 | 2193 | }, | |
| 2194 | 2194 | "delete_webhook": { | |
| 2210 | 2210 | }, | |
| 2211 | 2211 | "response": { | |
| 2212 | 2212 | "id": "dlv_01kpx5p9v2j7k3m8n4p5q6r7st", | |
| 2213 | − | "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 2214 | − | "eventId": "", | |
| 2213 | + | "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 2214 | + | "event_id": "", | |
| 2215 | 2215 | "event": "ping", | |
| 2216 | 2216 | "status": "delivered", | |
| 2217 | 2217 | "attempts": 1, | |
| 2218 | − | "responseStatus": 200, | |
| 2219 | − | "responseBody": "ok", | |
| 2218 | + | "response_status": 200, | |
| 2219 | + | "response_body": "ok", | |
| 2220 | 2220 | "error": null, | |
| 2221 | − | "durationMs": 184, | |
| 2221 | + | "duration_ms": 184, | |
| 2222 | 2222 | "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 | |
| 2226 | 2226 | }, | |
| 2227 | 2227 | "notes": "`payload` is the JSON that was sent, as a string." | |
| 2228 | 2228 | }, | |
| 2233 | 2233 | }, | |
| 2234 | 2234 | "response": { | |
| 2235 | 2235 | "id": "dlv_01kpx5p9v2j7k3m8n4p5q6r7st", | |
| 2236 | − | "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 2237 | − | "eventId": "", | |
| 2236 | + | "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 2237 | + | "event_id": "", | |
| 2238 | 2238 | "event": "ping", | |
| 2239 | 2239 | "status": "delivered", | |
| 2240 | 2240 | "attempts": 1, | |
| 2241 | − | "responseStatus": 200, | |
| 2242 | − | "responseBody": "ok", | |
| 2241 | + | "response_status": 200, | |
| 2242 | + | "response_body": "ok", | |
| 2243 | 2243 | "error": null, | |
| 2244 | − | "durationMs": 184, | |
| 2244 | + | "duration_ms": 184, | |
| 2245 | 2245 | "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 | |
| 2249 | 2249 | } | |
| 2250 | 2250 | }, | |
| 2251 | 2251 | "list_webhook_deliveries": { | |
| 2255 | 2255 | "response": [ | |
| 2256 | 2256 | { | |
| 2257 | 2257 | "id": "dlv_01kpx6a2b3c4d5e6f7g8h9j0kz", | |
| 2258 | − | "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 2259 | − | "eventId": "", | |
| 2258 | + | "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 2259 | + | "event_id": "", | |
| 2260 | 2260 | "event": "ping", | |
| 2261 | 2261 | "status": "pending", | |
| 2262 | 2262 | "attempts": 1, | |
| 2263 | − | "responseStatus": 503, | |
| 2264 | − | "responseBody": "Service Unavailable", | |
| 2263 | + | "response_status": 503, | |
| 2264 | + | "response_body": "Service Unavailable", | |
| 2265 | 2265 | "error": null, | |
| 2266 | − | "durationMs": 212, | |
| 2266 | + | "duration_ms": 212, | |
| 2267 | 2267 | "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" | |
| 2271 | 2271 | }, | |
| 2272 | 2272 | { | |
| 2273 | 2273 | "id": "dlv_01kpx5p9v2j7k3m8n4p5q6r7st", | |
| 2274 | − | "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 2275 | − | "eventId": "", | |
| 2274 | + | "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 2275 | + | "event_id": "", | |
| 2276 | 2276 | "event": "ping", | |
| 2277 | 2277 | "status": "delivered", | |
| 2278 | 2278 | "attempts": 1, | |
| 2279 | − | "responseStatus": 200, | |
| 2280 | − | "responseBody": "ok", | |
| 2279 | + | "response_status": 200, | |
| 2280 | + | "response_body": "ok", | |
| 2281 | 2281 | "error": null, | |
| 2282 | − | "durationMs": 184, | |
| 2282 | + | "duration_ms": 184, | |
| 2283 | 2283 | "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 | |
| 2287 | 2287 | } | |
| 2288 | 2288 | ], | |
| 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." | |
| 2290 | 2290 | }, | |
| 2291 | 2291 | "list_webhook_deliveries_for_workspace": { | |
| 2292 | 2292 | "params": { | |
| 2296 | 2296 | "response": [ | |
| 2297 | 2297 | { | |
| 2298 | 2298 | "id": "dlv_01kpx6a2b3c4d5e6f7g8h9j0kz", | |
| 2299 | − | "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 2300 | − | "eventId": "", | |
| 2299 | + | "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 2300 | + | "event_id": "", | |
| 2301 | 2301 | "event": "ping", | |
| 2302 | 2302 | "status": "pending", | |
| 2303 | 2303 | "attempts": 1, | |
| 2304 | − | "responseStatus": 503, | |
| 2305 | − | "responseBody": "Service Unavailable", | |
| 2304 | + | "response_status": 503, | |
| 2305 | + | "response_body": "Service Unavailable", | |
| 2306 | 2306 | "error": null, | |
| 2307 | − | "durationMs": 212, | |
| 2307 | + | "duration_ms": 212, | |
| 2308 | 2308 | "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" | |
| 2312 | 2312 | }, | |
| 2313 | 2313 | { | |
| 2314 | 2314 | "id": "dlv_01kpx5p9v2j7k3m8n4p5q6r7st", | |
| 2315 | − | "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 2316 | − | "eventId": "", | |
| 2315 | + | "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 2316 | + | "event_id": "", | |
| 2317 | 2317 | "event": "ping", | |
| 2318 | 2318 | "status": "delivered", | |
| 2319 | 2319 | "attempts": 1, | |
| 2320 | − | "responseStatus": 200, | |
| 2321 | − | "responseBody": "ok", | |
| 2320 | + | "response_status": 200, | |
| 2321 | + | "response_body": "ok", | |
| 2322 | 2322 | "error": null, | |
| 2323 | − | "durationMs": 184, | |
| 2323 | + | "duration_ms": 184, | |
| 2324 | 2324 | "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 | |
| 2328 | 2328 | } | |
| 2329 | 2329 | ] | |
| 2330 | 2330 | }, | |
| 2335 | 2335 | }, | |
| 2336 | 2336 | "response": { | |
| 2337 | 2337 | "id": "dlv_01kpx6c8d1e2f3g4h5j6k7m8np", | |
| 2338 | − | "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 2339 | − | "eventId": "", | |
| 2338 | + | "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 2339 | + | "event_id": "", | |
| 2340 | 2340 | "event": "ping", | |
| 2341 | 2341 | "status": "delivered", | |
| 2342 | 2342 | "attempts": 1, | |
| 2343 | − | "responseStatus": 200, | |
| 2344 | − | "responseBody": "ok", | |
| 2343 | + | "response_status": 200, | |
| 2344 | + | "response_body": "ok", | |
| 2345 | 2345 | "error": null, | |
| 2346 | − | "durationMs": 171, | |
| 2346 | + | "duration_ms": 171, | |
| 2347 | 2347 | "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 | |
| 2351 | 2351 | }, | |
| 2352 | 2352 | "notes": "The response is the new delivery." | |
| 2353 | 2353 | }, | |
| 2359 | 2359 | }, | |
| 2360 | 2360 | "response": { | |
| 2361 | 2361 | "id": "dlv_01kpx6c8d1e2f3g4h5j6k7m8np", | |
| 2362 | − | "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 2363 | − | "eventId": "", | |
| 2362 | + | "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 2363 | + | "event_id": "", | |
| 2364 | 2364 | "event": "ping", | |
| 2365 | 2365 | "status": "delivered", | |
| 2366 | 2366 | "attempts": 1, | |
| 2367 | − | "responseStatus": 200, | |
| 2368 | − | "responseBody": "ok", | |
| 2367 | + | "response_status": 200, | |
| 2368 | + | "response_body": "ok", | |
| 2369 | 2369 | "error": null, | |
| 2370 | − | "durationMs": 171, | |
| 2370 | + | "duration_ms": 171, | |
| 2371 | 2371 | "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 | |
| 2375 | 2375 | } | |
| 2376 | 2376 | }, | |
| 2377 | 2377 | "list_integrations": { | |
| 2387 | 2387 | "name": "Anthropic", | |
| 2388 | 2388 | "config": { | |
| 2389 | 2389 | "assign": false, | |
| 2390 | − | "writeBack": true | |
| 2390 | + | "write_back": true | |
| 2391 | 2391 | }, | |
| 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, | |
| 2398 | 2398 | "models": [ | |
| 2399 | 2399 | "claude-haiku-4-5", | |
| 2400 | 2400 | "claude-sonnet-4-5" | |
| 2408 | 2408 | "name": "Jira", | |
| 2409 | 2409 | "config": { | |
| 2410 | 2410 | "assign": false, | |
| 2411 | − | "writeBack": true, | |
| 2411 | + | "write_back": true, | |
| 2412 | 2412 | "site": "https://acme.atlassian.net", | |
| 2413 | 2413 | "email": "syntaqx@example.com", | |
| 2414 | 2414 | "keys": [ | |
| 2415 | 2415 | "TECH" | |
| 2416 | 2416 | ] | |
| 2417 | 2417 | }, | |
| 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, | |
| 2424 | 2424 | "models": [] | |
| 2425 | 2425 | } | |
| 2426 | 2426 | ], | |
| 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." | |
| 2428 | 2428 | }, | |
| 2429 | 2429 | "connect_integration": { | |
| 2430 | 2430 | "params": { | |
| 2448 | 2448 | "repo": "syntaqx/hello", | |
| 2449 | 2449 | "label": "bug", | |
| 2450 | 2450 | "assign": false, | |
| 2451 | − | "writeBack": true | |
| 2451 | + | "write_back": true | |
| 2452 | 2452 | }, | |
| 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, | |
| 2459 | 2459 | "models": [] | |
| 2460 | 2460 | }, | |
| 2461 | − | "signingSecret": "g1ts_9f2c4a7e1b0d3c6f8a2e5b7d9c1f4a6e8b0d2c4f6a8e1b3d" | |
| 2461 | + | "signing_secret": "g1ts_9f2c4a7e1b0d3c6f8a2e5b7d9c1f4a6e8b0d2c4f6a8e1b3d" | |
| 2462 | 2462 | }, | |
| 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/)." | |
| 2464 | 2464 | }, | |
| 2465 | 2465 | "disconnect_integration": { | |
| 2466 | 2466 | "params": { | |
| 2487 | 2487 | "response": [ | |
| 2488 | 2488 | { | |
| 2489 | 2489 | "task": "default", | |
| 2490 | − | "connectionId": "con_01kpx3m7q2f8r9t4v6w0y1z2ab", | |
| 2490 | + | "connection_id": "con_01kpx3m7q2f8r9t4v6w0y1z2ab", | |
| 2491 | 2491 | "model": "claude-sonnet-4-5" | |
| 2492 | 2492 | }, | |
| 2493 | 2493 | { | |
| 2494 | 2494 | "task": "review", | |
| 2495 | − | "connectionId": null, | |
| 2495 | + | "connection_id": null, | |
| 2496 | 2496 | "model": null | |
| 2497 | 2497 | } | |
| 2498 | 2498 | ] | |
| 2517 | 2517 | "response": [ | |
| 2518 | 2518 | { | |
| 2519 | 2519 | "task": "default", | |
| 2520 | − | "connectionId": "con_01kpx3m7q2f8r9t4v6w0y1z2ab", | |
| 2520 | + | "connection_id": "con_01kpx3m7q2f8r9t4v6w0y1z2ab", | |
| 2521 | 2521 | "model": "claude-sonnet-4-5" | |
| 2522 | 2522 | }, | |
| 2523 | 2523 | { | |
| 2524 | 2524 | "task": "review", | |
| 2525 | − | "connectionId": null, | |
| 2525 | + | "connection_id": null, | |
| 2526 | 2526 | "model": null | |
| 2527 | 2527 | } | |
| 2528 | 2528 | ], | |
| 2539 | 2539 | "url": "https://acme.atlassian.net/browse/TECH-1234", | |
| 2540 | 2540 | "status": "In Progress", | |
| 2541 | 2541 | "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" | |
| 2543 | 2543 | } | |
| 2544 | 2544 | }, | |
| 2545 | 2545 | "import_issue": { | |
| 2555 | 2555 | "url": "https://acme.atlassian.net/browse/TECH-1234", | |
| 2556 | 2556 | "status": "In Progress", | |
| 2557 | 2557 | "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" | |
| 2559 | 2559 | }, | |
| 2560 | 2560 | "created": true | |
| 2561 | 2561 | }, | |
| 2580 | 2580 | "kind": "gotcha", | |
| 2581 | 2581 | "source": { | |
| 2582 | 2582 | "kind": "run", | |
| 2583 | − | "runId": "arn_01m43v2c1p9k8d7e6f5a4b3c2d", | |
| 2583 | + | "run_id": "arn_01m43v2c1p9k8d7e6f5a4b3c2d", | |
| 2584 | 2584 | "repo": { | |
| 2585 | 2585 | "namespace": "syntaqx", | |
| 2586 | 2586 | "name": "hello" | |
| 2587 | 2587 | }, | |
| 2588 | 2588 | "number": 14 | |
| 2589 | 2589 | }, | |
| 2590 | − | "createdBy": "g1t-agent", | |
| 2590 | + | "created_by": "g1t-agent", | |
| 2591 | 2591 | "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 | |
| 2595 | 2595 | }, | |
| 2596 | 2596 | "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." | |
| 2597 | 2597 | }, | |
| 2611 | 2611 | "kind": "convention", | |
| 2612 | 2612 | "source": { | |
| 2613 | 2613 | "kind": "person", | |
| 2614 | − | "runId": null, | |
| 2614 | + | "run_id": null, | |
| 2615 | 2615 | "repo": null, | |
| 2616 | 2616 | "number": null | |
| 2617 | 2617 | }, | |
| 2618 | − | "createdBy": "syntaqx", | |
| 2618 | + | "created_by": "syntaqx", | |
| 2619 | 2619 | "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 | |
| 2623 | 2623 | } | |
| 2624 | 2624 | ] | |
| 2625 | 2625 | }, | |
| 2626 | 2626 | "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." | |
| 2627 | 2756 | } | |
| 2628 | 2757 | } |
| 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 | + | } |
| 130 | 130 | ), | |
| 131 | 131 | route( | |
| 132 | 132 | "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", | |
| 133 | 145 | "/workspaces/:workspace/integrations", | |
| 134 | 146 | Op::ListIntegrations, | |
| 135 | 147 | &[], |
| 23 | 23 | { "binding": "WEBHOOKS", "service": "g1t-webhooks" }, | |
| 24 | 24 | { "binding": "ACTIONS", "service": "g1t-actions" }, | |
| 25 | 25 | // 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" } | |
| 27 | 29 | ], | |
| 28 | 30 | // GitHub Actions artifacts and cache, in chunks, with KV's own expiry. | |
| 29 | 31 | // (Moves to R2 once R2 is enabled on the account.) |
| 45 | 45 | customCss: ['@g1t/theme/tokens.css', './src/styles/g1t.css'], | |
| 46 | 46 | social: [ | |
| 47 | 47 | { 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' }, | |
| 49 | 48 | ], | |
| 50 | 49 | editLink: { | |
| 51 | − | baseUrl: 'https://github.com/syntaqx/g1t/edit/main/apps/docs/', | |
| 50 | + | baseUrl: 'https://g1t.sh/syntaqx/g1t/blob/main/apps/docs/', | |
| 52 | 51 | }, | |
| 53 | 52 | lastUpdated: true, | |
| 54 | 53 | head: [ | |
| 82 | 81 | { label: 'Projects', slug: 'guides/projects' }, | |
| 83 | 82 | { label: 'Deployments', slug: 'guides/deployments' }, | |
| 84 | 83 | { label: 'Secrets and variables', slug: 'guides/secrets-and-variables' }, | |
| 84 | + | { label: 'Security', slug: 'guides/security' }, | |
| 85 | 85 | ], | |
| 86 | 86 | }, | |
| 87 | 87 | { | |
| 88 | 88 | label: 'Agents', | |
| 89 | 89 | items: [ | |
| 90 | 90 | { label: 'g1t agents', slug: 'guides/g1t-agents' }, | |
| 91 | + | { label: 'Guardrails', slug: 'guides/guardrails' }, | |
| 91 | 92 | { label: 'Outcomes and plans', slug: 'guides/outcomes' }, | |
| 92 | 93 | { label: 'Talking to agents', slug: 'guides/talking-to-agents' }, | |
| 93 | 94 | { label: 'Agents, sessions and memory', slug: 'guides/agents-and-memory' }, | |
| 95 | + | { label: 'Context hub', slug: 'guides/context-hub' }, | |
| 94 | 96 | { label: 'Bring your own agent', slug: 'guides/bring-your-own-agent' }, | |
| 95 | 97 | ], | |
| 96 | 98 | }, | |
| 117 | 119 | items: [ | |
| 118 | 120 | { label: 'Accounts and sign-in', slug: 'guides/authentication' }, | |
| 119 | 121 | { label: 'Workspaces and tokens', slug: 'guides/workspaces' }, | |
| 122 | + | { label: 'Audit log', slug: 'guides/audit-log' }, | |
| 120 | 123 | { label: 'Usage and billing', slug: 'guides/usage-and-billing' }, | |
| 121 | 124 | { label: 'Git', slug: 'guides/git' }, | |
| 122 | 125 | ], |
| 251 | 251 | out.push( | |
| 252 | 252 | '## Body parameters', | |
| 253 | 253 | '', | |
| 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.', | |
| 255 | 255 | '', | |
| 256 | 256 | table(['Name', 'Type', 'Required', 'Description'], propertyRows(body)), | |
| 257 | 257 | '', |
| 5 | 5 | <nav class="g1t-nav" aria-label="g1t"> | |
| 6 | 6 | <a href="https://g1t.sh/">g1t.sh</a> | |
| 7 | 7 | <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> | |
| 13 | 15 | </svg> | |
| 14 | 16 | </a> | |
| 15 | 17 | </nav> |
| 1 | 1 | 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'; | |
| 4 | 4 | ||
| 5 | 5 | export const collections = { | |
| 6 | 6 | 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() }), | |
| 7 | 9 | }; |
| 6 | 6 | g1t is a git forge for teams of agents. You hand g1t an outcome, and a team | |
| 7 | 7 | of agents converges it onto `main`: each change is made in a pull request | |
| 8 | 8 | 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. | |
| 12 | 11 | ||
| 13 | 12 | Underneath it is ordinary git: repositories, commits, branches, clone, push | |
| 14 | 13 | and pull all work as they do anywhere. On top of that it has the two things |
| 15 | 15 | | See how a pull request was made | **Agents → Sessions**, or the pull request's Agent panel | | |
| 16 | 16 | | Teach every agent something about this code | **Agents → Memory** | | |
| 17 | 17 | | 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 | | |
| 18 | 19 | | See every agent across the workspace, and its cost | **Agent fleet**, under Across projects | | |
| 19 | 20 | ||
| 20 | 21 | ## Runs | |
| 142 | 143 | or the agent's run and the pull request it was working on, linked from the | |
| 143 | 144 | memory. | |
| 144 | 145 | ||
| 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 | + | ||
| 145 | 162 | Your own agents can use memory too, through the [MCP tools](/reference/mcp/#memory) | |
| 146 | 163 | `remember` and `recall` or the API: | |
| 147 | 164 | ||
| 166 | 183 | - **add** a memory, with its kind: fact, convention, decision or gotcha; | |
| 167 | 184 | - **pin** one, so every agent gets it first, whatever the budget; | |
| 168 | 185 | - **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. | |
| 170 | 189 | ||
| 171 | 190 | Each shows who added it, when, and when it was last given to an agent. A | |
| 172 | 191 | memory that has not been given to an agent for a long time is a good one to |
| 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. |
| 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/). |
| 42 | 42 | The same thing is the `assign_issue` tool on the MCP server, so an agent | |
| 43 | 43 | planning work can hand issues to g1t agents itself. | |
| 44 | 44 | ||
| 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 | + | ||
| 45 | 51 | Each agent appears as a draft pull request on its issue within a few | |
| 46 | 52 | seconds. The pages update on their own while they work. | |
| 47 | 53 | ||
| 68 | 74 | If an agent fails, or finishes without changing anything, its pull request | |
| 69 | 75 | is closed and its session says why. | |
| 70 | 76 | ||
| 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 | + | ||
| 71 | 123 | ## Seeing it through | |
| 72 | 124 | ||
| 73 | 125 | Making the change is the first step. g1t takes the rest itself, and you | |
| 172 | 224 | ||
| 173 | 225 | Both run in sandboxes of their own. | |
| 174 | 226 | ||
| 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 | + | ||
| 175 | 275 | ## Which model runs | |
| 176 | 276 | ||
| 177 | 277 | You do not pick one. You assign the work to `g1t-agent`, the way you would | |
| 234 | 334 | [its own model provider](/guides/models/), and, until October 22, in any | |
| 235 | 335 | workspace on its free $1 of g1t's own models. See | |
| 236 | 336 | [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 | |
| 241 | 338 | longer push or report. | |
| 242 | 339 | ||
| 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 | + | ||
| 243 | 397 | ## What a sandbox can reach | |
| 244 | 398 | ||
| 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. |
| 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. |
| 223 | 223 | `openai_endpoint`, `sentry`, `datadog`, `webhook`, `jira` or `linear`. `config` takes `repo`, `assign`, `label`, | |
| 224 | 224 | `write_back`, `organization`, `site`, `email`, `keys`, `base_url`, | |
| 225 | 225 | `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 | |
| 227 | 227 | is shown. |
| 173 | 173 | "agent": "g1t-agent", | |
| 174 | 174 | "state": "testing", | |
| 175 | 175 | "ahead": [41], | |
| 176 | − | "baseCommit": "8f3c2e1…", | |
| 177 | − | "combinedCommit": null, | |
| 176 | + | "base_commit": "8f3c2e1…", | |
| 177 | + | "combined_commit": null, | |
| 178 | 178 | "results": [], | |
| 179 | − | "enqueuedBy": "g1t" | |
| 179 | + | "enqueued_by": "g1t" | |
| 180 | 180 | } | |
| 181 | 181 | ], | |
| 182 | 182 | "recent": [] | |
| 190 | 190 | | `recent` | Those that landed or left, newest first. | | |
| 191 | 191 | | `state` | `waiting`, `testing`, `passed`, `failed`, `landed` or `removed`. | | |
| 192 | 192 | | `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. | | |
| 195 | 195 | | `results` | The checks run against it, each with `command`, `passed` and `output`. | | |
| 196 | 196 | | `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. | |
| 132 | 132 | ||
| 133 | 133 | | Tool | Route | | | |
| 134 | 134 | | --- | --- | --- | | |
| 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. | | |
| 136 | 136 | | `get_plan` | `GET /repos/{owner}/{name}/plans/{plan}` | The plan, its `status` and the issues it proposes. | | |
| 137 | 137 | | `apply_plan` | `POST /repos/{owner}/{name}/plans/{plan}/apply` | Open its issues. Body: `assign`, `keep`. | | |
| 138 | 138 | ||
| 156 | 156 | ||
| 157 | 157 | A plan's `status` is `planning`, `ready`, `failed` or `applied`. Each | |
| 158 | 158 | 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, | |
| 160 | 160 | `number`. `keep` takes positions counting from 1; leave it out to open | |
| 161 | 161 | every issue. | |
| 162 | 162 | ||
| 164 | 164 | ||
| 165 | 165 | | Field | | | |
| 166 | 166 | | --- | --- | | |
| 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`. | | |
| 168 | 168 | | `exchanges` | The questions and handoffs between the agents on its pull requests. | |
| 118 | 118 | ||
| 119 | 119 | | Field | What it is | | |
| 120 | 120 | | --- | --- | | |
| 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. | | |
| 123 | 123 | | `statuses` | What each workflow run said about its head. | | |
| 124 | 124 | | `mergeable` | `clean`, `conflicting`, `checking` or `unknown`. | | |
| 125 | 125 | | `conflicts` | When conflicting, the files that conflict. | |
| 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. |
| 50 | 50 | -d '{"body": "Keep the old flag working too."}' | |
| 51 | 51 | ``` | |
| 52 | 52 | ||
| 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 | |
| 54 | 54 | received it. | |
| 55 | 55 | ||
| 56 | 56 | ## Ask for changes |
| 36 | 36 | "type": "comment.created", | |
| 37 | 37 | "time": "2026-10-03T04:54:37.708Z", | |
| 38 | 38 | "workspace": "acme", | |
| 39 | − | "repository": { "id": "rep_cf985171afeee00a62a1c0acb0", "fullName": "acme/web" }, | |
| 39 | + | "repository": { "id": "rep_cf985171afeee00a62a1c0acb0", "full_name": "acme/web" }, | |
| 40 | 40 | "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" } | |
| 42 | 42 | } | |
| 43 | 43 | ``` | |
| 44 | 44 | ||
| 60 | 60 | ||
| 61 | 61 | | Event | When | | |
| 62 | 62 | | --- | --- | | |
| 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`. | | |
| 64 | 64 | | `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`. | | |
| 66 | 66 | | `comment.created` | A comment or review on an issue or pull request. | | |
| 67 | 67 | | `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`. | | |
| 68 | 68 | | `checks.completed` | An issue's acceptance checks finished on a pull request. `data.status` is `passed`, `failed` or `errored`. | | |
| 69 | 69 | | `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`. | | |
| 71 | 71 | | `queue.changed` | The merge queue gained, lost or settled an entry. | | |
| 72 | 72 | | `session.appended` | An agent's session grew. Busy: choose it only if you need it. | | |
| 73 | 73 | | `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. | |
| 49 | 49 | Every pull request gets a [live preview on g1t.page](/guides/deployments/), and | |
| 50 | 50 | the default branch goes to production. Apps cost nothing while no one visits. | |
| 51 | 51 | </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> | |
| 52 | 56 | <Card title="Your models, your tools" icon="puzzle"> | |
| 53 | 57 | Use g1t's models or [your own providers](/guides/models/), and pull context | |
| 54 | 58 | from [Sentry, Jira and Linear](/guides/integrations/). |
| 78 | 78 | ||
| 79 | 79 | Request bodies are JSON. | |
| 80 | 80 | ||
| 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: | |
| 84 | 84 | ||
| 85 | 85 | ```sh | |
| 86 | 86 | # Both turn off counting agents' approvals. | |
| 92 | 92 | ||
| 93 | 93 | When a body gives a field both ways, the `snake_case` one is used. | |
| 94 | 94 | ||
| 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 | + | ||
| 95 | 99 | A successful request answers `200` with the result as the body: an object, | |
| 96 | 100 | a list, or `true` for a deletion. There is no envelope around it. | |
| 97 | 101 |
| 13 | 13 | - `repo` is always `owner/name`, such as `"syntaqx/hello"`. | |
| 14 | 14 | - `number` names an issue or a pull request. The two share one sequence per | |
| 15 | 15 | 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. | |
| 17 | 18 | - A tool that fails returns its error as the result, with `isError` set, so | |
| 18 | 19 | the agent can read it and act on it. | |
| 19 | 20 | - Reading a public repository needs no sign-in through the API. Through MCP, | |
| 89 | 90 | | --- | --- | --- | --- | | |
| 90 | 91 | | `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/) | | |
| 91 | 92 | | `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/) | | |
| 92 | 95 | ||
| 93 | 96 | See [agents, sessions and memory](/guides/agents-and-memory/). | |
| 94 | 97 | ||
| 174 | 177 | ||
| 175 | 178 | ## What a g1t agent can use | |
| 176 | 179 | ||
| 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. |
| 1 | + | { | |
| 2 | + | "page.editLink": "View this page on g1t" | |
| 3 | + | } |
| 3 | 3 | "info": { | |
| 4 | 4 | "title": "g1t API", | |
| 5 | 5 | "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.", | |
| 7 | 7 | "license": { | |
| 8 | 8 | "name": "MIT", | |
| 9 | 9 | "identifier": "MIT" | |
| 107 | 107 | ] | |
| 108 | 108 | }, | |
| 109 | 109 | { | |
| 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 | + | { | |
| 110 | 118 | "name": "Actions", | |
| 111 | 119 | "description": "GitHub Actions workflows in .g1t/workflows, their runs, and their jobs' logs.", | |
| 112 | 120 | "x-tools": [ | |
| 399 | 407 | "slug": "syntaqx-labs", | |
| 400 | 408 | "name": "Syntaqx Labs", | |
| 401 | 409 | "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 | |
| 404 | 412 | } | |
| 405 | 413 | } | |
| 406 | 414 | } | |
| 523 | 531 | "namespace": "syntaqx", | |
| 524 | 532 | "name": "hello", | |
| 525 | 533 | "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, | |
| 530 | 538 | "protected": true, | |
| 531 | − | "createdAt": "2026-09-28T14:11:52.640Z" | |
| 539 | + | "created_at": "2026-09-28T14:11:52.640Z" | |
| 532 | 540 | } | |
| 533 | 541 | ] | |
| 534 | 542 | } | |
| 575 | 583 | "Repositories" | |
| 576 | 584 | ], | |
| 577 | 585 | "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.", | |
| 579 | 587 | "x-mcp-tool": "create_repo", | |
| 580 | 588 | "security": [ | |
| 581 | 589 | { | |
| 594 | 602 | "namespace": "syntaqx", | |
| 595 | 603 | "name": "hello-cli", | |
| 596 | 604 | "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, | |
| 601 | 609 | "protected": false, | |
| 602 | − | "createdAt": "2026-10-04T16:05:12.913Z" | |
| 610 | + | "created_at": "2026-10-04T16:05:12.913Z" | |
| 603 | 611 | } | |
| 604 | 612 | } | |
| 605 | 613 | } | |
| 741 | 749 | "namespace": "syntaqx", | |
| 742 | 750 | "name": "hello", | |
| 743 | 751 | "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, | |
| 748 | 756 | "protected": true, | |
| 749 | − | "createdAt": "2026-09-28T14:11:52.640Z" | |
| 757 | + | "created_at": "2026-09-28T14:11:52.640Z" | |
| 750 | 758 | } | |
| 751 | 759 | } | |
| 752 | 760 | } | |
| 837 | 845 | "namespace": "syntaqx", | |
| 838 | 846 | "name": "hello", | |
| 839 | 847 | "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, | |
| 844 | 852 | "protected": true, | |
| 845 | − | "createdAt": "2026-09-28T14:11:52.640Z" | |
| 853 | + | "created_at": "2026-09-28T14:11:52.640Z" | |
| 846 | 854 | } | |
| 847 | 855 | } | |
| 848 | 856 | } | |
| 969 | 977 | "application/json": { | |
| 970 | 978 | "schema": {}, | |
| 971 | 979 | "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 | |
| 982 | 990 | } | |
| 983 | 991 | } | |
| 984 | 992 | } | |
| 1065 | 1073 | "application/json": { | |
| 1066 | 1074 | "schema": {}, | |
| 1067 | 1075 | "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" | |
| 1078 | 1086 | } | |
| 1079 | 1087 | } | |
| 1080 | 1088 | } | |
| 1232 | 1240 | "agent": "claude-code", | |
| 1233 | 1241 | "state": "testing", | |
| 1234 | 1242 | "ahead": [], | |
| 1235 | − | "baseCommit": "3a7e9c1b5d2f4a6c8e0b2d4f6a8c0e2b4d6f8a1c", | |
| 1236 | − | "combinedCommit": null, | |
| 1243 | + | "base_commit": "3a7e9c1b5d2f4a6c8e0b2d4f6a8c0e2b4d6f8a1c", | |
| 1244 | + | "combined_commit": null, | |
| 1237 | 1245 | "error": null, | |
| 1238 | 1246 | "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 | |
| 1242 | 1250 | } | |
| 1243 | 1251 | ], | |
| 1244 | 1252 | "recent": [ | |
| 1249 | 1257 | "agent": "g1t-agent", | |
| 1250 | 1258 | "state": "landed", | |
| 1251 | 1259 | "ahead": [], | |
| 1252 | − | "baseCommit": "4b1d7e9c2a5f8e3d6c0b9a7e5d3c1b8a6f4e2d0c", | |
| 1253 | − | "combinedCommit": "c3e8a1f6d4b2097e5c3a1f8d6b4e2c0a9f7d5b3e", | |
| 1260 | + | "base_commit": "4b1d7e9c2a5f8e3d6c0b9a7e5d3c1b8a6f4e2d0c", | |
| 1261 | + | "combined_commit": "c3e8a1f6d4b2097e5c3a1f8d6b4e2c0a9f7d5b3e", | |
| 1254 | 1262 | "error": null, | |
| 1255 | 1263 | "results": [ | |
| 1256 | 1264 | { | |
| 1257 | 1265 | "command": "cargo test", | |
| 1258 | 1266 | "passed": true, | |
| 1259 | − | "exitCode": 0, | |
| 1267 | + | "exit_code": 0, | |
| 1260 | 1268 | "output": "test result: ok. 12 passed; 0 failed\n", | |
| 1261 | − | "durationMs": 48211 | |
| 1269 | + | "duration_ms": 48211 | |
| 1262 | 1270 | } | |
| 1263 | 1271 | ], | |
| 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" | |
| 1267 | 1275 | } | |
| 1268 | 1276 | ] | |
| 1269 | 1277 | } | |
| 1320 | 1328 | "Pull requests" | |
| 1321 | 1329 | ], | |
| 1322 | 1330 | "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.", | |
| 1324 | 1332 | "x-mcp-tool": "message_agent", | |
| 1325 | 1333 | "security": [ | |
| 1326 | 1334 | { | |
| 1366 | 1374 | "id": "msg_01m43tx5egeh4t3f9zcnjenqvz", | |
| 1367 | 1375 | "author": "syntaqx", | |
| 1368 | 1376 | "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, | |
| 1371 | 1379 | "kind": "message", | |
| 1372 | − | "fromNumber": null, | |
| 1373 | − | "toNumber": 14, | |
| 1380 | + | "from_number": null, | |
| 1381 | + | "to_number": 14, | |
| 1374 | 1382 | "answer": null, | |
| 1375 | 1383 | "declined": false | |
| 1376 | 1384 | } | |
| 1518 | 1526 | "id": "msg_01m43tx5egeh4t3f9zcnjenqvz", | |
| 1519 | 1527 | "author": "syntaqx", | |
| 1520 | 1528 | "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", | |
| 1523 | 1531 | "kind": "message", | |
| 1524 | − | "fromNumber": null, | |
| 1525 | − | "toNumber": 14, | |
| 1532 | + | "from_number": null, | |
| 1533 | + | "to_number": 14, | |
| 1526 | 1534 | "answer": null, | |
| 1527 | 1535 | "declined": false | |
| 1528 | 1536 | } | |
| 1636 | 1644 | "id": "msg_01m43t66tde8hs2vpxhh3v8tqw", | |
| 1637 | 1645 | "author": "g1t-agent", | |
| 1638 | 1646 | "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", | |
| 1641 | 1649 | "kind": "question", | |
| 1642 | − | "fromNumber": 16, | |
| 1643 | − | "toNumber": 14, | |
| 1650 | + | "from_number": 16, | |
| 1651 | + | "to_number": 14, | |
| 1644 | 1652 | "answer": "No. greet() keeps its name; only greet_named() is added.", | |
| 1645 | 1653 | "declined": false | |
| 1646 | 1654 | } | |
| 1781 | 1789 | "kind": "gotcha", | |
| 1782 | 1790 | "source": { | |
| 1783 | 1791 | "kind": "run", | |
| 1784 | − | "runId": "arn_01m43v2c1p9k8d7e6f5a4b3c2d", | |
| 1792 | + | "run_id": "arn_01m43v2c1p9k8d7e6f5a4b3c2d", | |
| 1785 | 1793 | "repo": { | |
| 1786 | 1794 | "namespace": "syntaqx", | |
| 1787 | 1795 | "name": "hello" | |
| 1788 | 1796 | }, | |
| 1789 | 1797 | "number": 14 | |
| 1790 | 1798 | }, | |
| 1791 | − | "createdBy": "g1t-agent", | |
| 1799 | + | "created_by": "g1t-agent", | |
| 1792 | 1800 | "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 | |
| 1796 | 1804 | } | |
| 1797 | 1805 | } | |
| 1798 | 1806 | } | |
| 1965 | 1973 | "kind": "convention", | |
| 1966 | 1974 | "source": { | |
| 1967 | 1975 | "kind": "person", | |
| 1968 | − | "runId": null, | |
| 1976 | + | "run_id": null, | |
| 1969 | 1977 | "repo": null, | |
| 1970 | 1978 | "number": null | |
| 1971 | 1979 | }, | |
| 1972 | − | "createdBy": "syntaqx", | |
| 1980 | + | "created_by": "syntaqx", | |
| 1973 | 1981 | "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 | |
| 1977 | 1985 | } | |
| 1978 | 1986 | ] | |
| 1979 | 1987 | } | |
| 2082 | 2090 | "type": "pull.opened", | |
| 2083 | 2091 | "source": "work", | |
| 2084 | 2092 | "time": "2026-10-01T18:20:02.117Z", | |
| 2085 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2093 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2086 | 2094 | "actor": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2087 | 2095 | "data": { | |
| 2088 | − | "pullId": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 2089 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2096 | + | "pull_id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 2097 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2090 | 2098 | "number": 14, | |
| 2091 | 2099 | "issue": 12, | |
| 2092 | 2100 | "agent": "claude-code" | |
| 2097 | 2105 | "type": "issue.opened", | |
| 2098 | 2106 | "source": "work", | |
| 2099 | 2107 | "time": "2026-10-01T18:04:11.482Z", | |
| 2100 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2108 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2101 | 2109 | "actor": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2102 | 2110 | "data": { | |
| 2103 | − | "issueId": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 2104 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2111 | + | "issue_id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 2112 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2105 | 2113 | "number": 12, | |
| 2106 | 2114 | "title": "Greeting should name the caller" | |
| 2107 | 2115 | } | |
| 2258 | 2266 | "Issues" | |
| 2259 | 2267 | ], | |
| 2260 | 2268 | "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\".", | |
| 2262 | 2270 | "x-mcp-tool": "list_issues", | |
| 2263 | 2271 | "security": [ | |
| 2264 | 2272 | { | |
| 2316 | 2324 | "example": [ | |
| 2317 | 2325 | { | |
| 2318 | 2326 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 2319 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2327 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2320 | 2328 | "number": 12, | |
| 2321 | 2329 | "title": "Greeting should name the caller", | |
| 2322 | 2330 | "body": "Take a name from the first argument; fall back to world.", | |
| 2328 | 2336 | ], | |
| 2329 | 2337 | "state": "open", | |
| 2330 | 2338 | "reason": null, | |
| 2331 | − | "resolvedBy": null, | |
| 2339 | + | "resolved_by": null, | |
| 2332 | 2340 | "author": { | |
| 2333 | 2341 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2334 | 2342 | "username": "syntaqx", | |
| 2336 | 2344 | "verified": false, | |
| 2337 | 2345 | "workspaces": [] | |
| 2338 | 2346 | }, | |
| 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, | |
| 2344 | 2352 | "assignees": [], | |
| 2345 | − | "blockedBy": [], | |
| 2353 | + | "blocked_by": [], | |
| 2346 | 2354 | "queued": false, | |
| 2347 | 2355 | "agent": "claude-code" | |
| 2348 | 2356 | } | |
| 2437 | 2445 | "schema": {}, | |
| 2438 | 2446 | "example": { | |
| 2439 | 2447 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 2440 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2448 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2441 | 2449 | "number": 12, | |
| 2442 | 2450 | "title": "Greeting should name the caller", | |
| 2443 | 2451 | "body": "Take a name from the first argument; fall back to world.", | |
| 2449 | 2457 | ], | |
| 2450 | 2458 | "state": "open", | |
| 2451 | 2459 | "reason": null, | |
| 2452 | − | "resolvedBy": null, | |
| 2460 | + | "resolved_by": null, | |
| 2453 | 2461 | "author": { | |
| 2454 | 2462 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2455 | 2463 | "username": "syntaqx", | |
| 2457 | 2465 | "verified": false, | |
| 2458 | 2466 | "workspaces": [] | |
| 2459 | 2467 | }, | |
| 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, | |
| 2465 | 2473 | "assignees": [], | |
| 2466 | − | "blockedBy": [], | |
| 2474 | + | "blocked_by": [], | |
| 2467 | 2475 | "queued": false, | |
| 2468 | 2476 | "agent": null | |
| 2469 | 2477 | } | |
| 2577 | 2585 | "Issues" | |
| 2578 | 2586 | ], | |
| 2579 | 2587 | "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.", | |
| 2581 | 2589 | "x-mcp-tool": "get_issue", | |
| 2582 | 2590 | "security": [ | |
| 2583 | 2591 | { | |
| 2623 | 2631 | "example": { | |
| 2624 | 2632 | "issue": { | |
| 2625 | 2633 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 2626 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2634 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2627 | 2635 | "number": 12, | |
| 2628 | 2636 | "title": "Greeting should name the caller", | |
| 2629 | 2637 | "body": "Take a name from the first argument; fall back to world.", | |
| 2635 | 2643 | ], | |
| 2636 | 2644 | "state": "closed", | |
| 2637 | 2645 | "reason": "completed", | |
| 2638 | − | "resolvedBy": 14, | |
| 2646 | + | "resolved_by": 14, | |
| 2639 | 2647 | "author": { | |
| 2640 | 2648 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2641 | 2649 | "username": "syntaqx", | |
| 2643 | 2651 | "verified": false, | |
| 2644 | 2652 | "workspaces": [] | |
| 2645 | 2653 | }, | |
| 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, | |
| 2651 | 2659 | "assignees": [], | |
| 2652 | − | "blockedBy": [], | |
| 2660 | + | "blocked_by": [], | |
| 2653 | 2661 | "queued": false, | |
| 2654 | 2662 | "agent": null | |
| 2655 | 2663 | }, | |
| 2656 | 2664 | "pulls": [ | |
| 2657 | 2665 | { | |
| 2658 | 2666 | "id": "pr_01m43sjq8tcz5rbm2a7k4d9e6w", | |
| 2659 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2667 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2660 | 2668 | "number": 13, | |
| 2661 | 2669 | "issue": 12, | |
| 2662 | 2670 | "title": "Greeting should name the caller", | |
| 2668 | 2676 | "namespace": "pulls", | |
| 2669 | 2677 | "name": "pr_01m43sjq8tcz5rbm2a7k4d9e6w" | |
| 2670 | 2678 | }, | |
| 2671 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 2679 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 2672 | 2680 | "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, | |
| 2679 | 2687 | "files": [], | |
| 2680 | 2688 | "assignees": [], | |
| 2681 | 2689 | "reviewers": [], | |
| 2686 | 2694 | "verified": false, | |
| 2687 | 2695 | "workspaces": [] | |
| 2688 | 2696 | }, | |
| 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" | |
| 2691 | 2699 | }, | |
| 2692 | 2700 | { | |
| 2693 | 2701 | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 2694 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2702 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2695 | 2703 | "number": 14, | |
| 2696 | 2704 | "issue": 12, | |
| 2697 | 2705 | "title": "Greeting should name the caller", | |
| 2703 | 2711 | "namespace": "pulls", | |
| 2704 | 2712 | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 2705 | 2713 | }, | |
| 2706 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 2714 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 2707 | 2715 | "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", | |
| 2714 | 2722 | "files": [ | |
| 2715 | 2723 | { | |
| 2716 | 2724 | "path": "src/main.rs", | |
| 2729 | 2737 | "verified": false, | |
| 2730 | 2738 | "workspaces": [] | |
| 2731 | 2739 | }, | |
| 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" | |
| 2734 | 2742 | } | |
| 2735 | 2743 | ], | |
| 2736 | 2744 | "comments": [ | |
| 2748 | 2756 | "path": null, | |
| 2749 | 2757 | "line": null, | |
| 2750 | 2758 | "verdict": null, | |
| 2751 | − | "createdAt": "2026-10-01T18:20:02.141Z" | |
| 2759 | + | "created_at": "2026-10-01T18:20:02.141Z" | |
| 2752 | 2760 | } | |
| 2753 | 2761 | ] | |
| 2754 | 2762 | } | |
| 2847 | 2855 | "schema": {}, | |
| 2848 | 2856 | "example": { | |
| 2849 | 2857 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 2850 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2858 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 2851 | 2859 | "number": 12, | |
| 2852 | 2860 | "title": "Greeting should name the caller", | |
| 2853 | 2861 | "body": "Take a name from the first argument; fall back to world.", | |
| 2860 | 2868 | ], | |
| 2861 | 2869 | "state": "open", | |
| 2862 | 2870 | "reason": null, | |
| 2863 | − | "resolvedBy": null, | |
| 2871 | + | "resolved_by": null, | |
| 2864 | 2872 | "author": { | |
| 2865 | 2873 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 2866 | 2874 | "username": "syntaqx", | |
| 2868 | 2876 | "verified": false, | |
| 2869 | 2877 | "workspaces": [] | |
| 2870 | 2878 | }, | |
| 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, | |
| 2876 | 2884 | "assignees": [ | |
| 2877 | 2885 | "syntaqx" | |
| 2878 | 2886 | ], | |
| 2879 | − | "blockedBy": [], | |
| 2887 | + | "blocked_by": [], | |
| 2880 | 2888 | "queued": false, | |
| 2881 | 2889 | "agent": null | |
| 2882 | 2890 | } | |
| 3027 | 3035 | "schema": {}, | |
| 3028 | 3036 | "example": { | |
| 3029 | 3037 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 3030 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 3038 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 3031 | 3039 | "number": 12, | |
| 3032 | 3040 | "title": "Greeting should name the caller", | |
| 3033 | 3041 | "body": "Take a name from the first argument; fall back to world.", | |
| 3039 | 3047 | ], | |
| 3040 | 3048 | "state": "closed", | |
| 3041 | 3049 | "reason": "not_planned", | |
| 3042 | − | "resolvedBy": null, | |
| 3050 | + | "resolved_by": null, | |
| 3043 | 3051 | "author": { | |
| 3044 | 3052 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 3045 | 3053 | "username": "syntaqx", | |
| 3047 | 3055 | "verified": false, | |
| 3048 | 3056 | "workspaces": [] | |
| 3049 | 3057 | }, | |
| 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, | |
| 3055 | 3063 | "assignees": [], | |
| 3056 | − | "blockedBy": [], | |
| 3064 | + | "blocked_by": [], | |
| 3057 | 3065 | "queued": false, | |
| 3058 | 3066 | "agent": null | |
| 3059 | 3067 | } | |
| 3187 | 3195 | "schema": {}, | |
| 3188 | 3196 | "example": { | |
| 3189 | 3197 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 3190 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 3198 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 3191 | 3199 | "number": 12, | |
| 3192 | 3200 | "title": "Greeting should name the caller", | |
| 3193 | 3201 | "body": "Take a name from the first argument; fall back to world.", | |
| 3199 | 3207 | ], | |
| 3200 | 3208 | "state": "open", | |
| 3201 | 3209 | "reason": null, | |
| 3202 | − | "resolvedBy": null, | |
| 3210 | + | "resolved_by": null, | |
| 3203 | 3211 | "author": { | |
| 3204 | 3212 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 3205 | 3213 | "username": "syntaqx", | |
| 3207 | 3215 | "verified": false, | |
| 3208 | 3216 | "workspaces": [] | |
| 3209 | 3217 | }, | |
| 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, | |
| 3215 | 3223 | "assignees": [], | |
| 3216 | − | "blockedBy": [], | |
| 3224 | + | "blocked_by": [], | |
| 3217 | 3225 | "queued": false, | |
| 3218 | 3226 | "agent": null | |
| 3219 | 3227 | } | |
| 3324 | 3332 | "schema": {}, | |
| 3325 | 3333 | "example": { | |
| 3326 | 3334 | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 3327 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 3335 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 3328 | 3336 | "number": 14, | |
| 3329 | 3337 | "issue": 12, | |
| 3330 | 3338 | "title": "Greeting should name the caller", | |
| 3336 | 3344 | "namespace": "pulls", | |
| 3337 | 3345 | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 3338 | 3346 | }, | |
| 3339 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 3347 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 3340 | 3348 | "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, | |
| 3347 | 3355 | "files": [], | |
| 3348 | 3356 | "assignees": [], | |
| 3349 | 3357 | "reviewers": [], | |
| 3354 | 3362 | "verified": false, | |
| 3355 | 3363 | "workspaces": [] | |
| 3356 | 3364 | }, | |
| 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" | |
| 3359 | 3367 | } | |
| 3360 | 3368 | } | |
| 3361 | 3369 | } | |
| 3491 | 3499 | "url": "https://acme.atlassian.net/browse/TECH-1234", | |
| 3492 | 3500 | "status": "In Progress", | |
| 3493 | 3501 | "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" | |
| 3495 | 3503 | }, | |
| 3496 | 3504 | "created": true | |
| 3497 | 3505 | } | |
| 3633 | 3641 | "url": "https://acme.atlassian.net/browse/TECH-1234", | |
| 3634 | 3642 | "status": "In Progress", | |
| 3635 | 3643 | "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" | |
| 3637 | 3645 | } | |
| 3638 | 3646 | } | |
| 3639 | 3647 | } | |
| 3684 | 3692 | } | |
| 3685 | 3693 | } | |
| 3686 | 3694 | }, | |
| 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 | + | }, | |
| 3687 | 4053 | "/workspaces/{workspace}/integrations": { | |
| 3688 | 4054 | "get": { | |
| 3689 | 4055 | "operationId": "list_integrations", | |
| 3691 | 4057 | "Integrations" | |
| 3692 | 4058 | ], | |
| 3693 | 4059 | "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.", | |
| 3695 | 4061 | "x-mcp-tool": "list_integrations", | |
| 3696 | 4062 | "security": [ | |
| 3697 | 4063 | { | |
| 3724 | 4090 | "name": "Anthropic", | |
| 3725 | 4091 | "config": { | |
| 3726 | 4092 | "assign": false, | |
| 3727 | − | "writeBack": true | |
| 4093 | + | "write_back": true | |
| 3728 | 4094 | }, | |
| 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, | |
| 3735 | 4101 | "models": [ | |
| 3736 | 4102 | "claude-haiku-4-5", | |
| 3737 | 4103 | "claude-sonnet-4-5" | |
| 3745 | 4111 | "name": "Jira", | |
| 3746 | 4112 | "config": { | |
| 3747 | 4113 | "assign": false, | |
| 3748 | − | "writeBack": true, | |
| 4114 | + | "write_back": true, | |
| 3749 | 4115 | "site": "https://acme.atlassian.net", | |
| 3750 | 4116 | "email": "syntaqx@example.com", | |
| 3751 | 4117 | "keys": [ | |
| 3752 | 4118 | "TECH" | |
| 3753 | 4119 | ] | |
| 3754 | 4120 | }, | |
| 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, | |
| 3761 | 4127 | "models": [] | |
| 3762 | 4128 | } | |
| 3763 | 4129 | ] | |
| 3815 | 4181 | "Integrations" | |
| 3816 | 4182 | ], | |
| 3817 | 4183 | "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/).", | |
| 3819 | 4185 | "x-mcp-tool": "connect_integration", | |
| 3820 | 4186 | "security": [ | |
| 3821 | 4187 | { | |
| 3850 | 4216 | "repo": "syntaqx/hello", | |
| 3851 | 4217 | "label": "bug", | |
| 3852 | 4218 | "assign": false, | |
| 3853 | − | "writeBack": true | |
| 4219 | + | "write_back": true | |
| 3854 | 4220 | }, | |
| 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, | |
| 3861 | 4227 | "models": [] | |
| 3862 | 4228 | }, | |
| 3863 | − | "signingSecret": "g1ts_9f2c4a7e1b0d3c6f8a2e5b7d9c1f4a6e8b0d2c4f6a8e1b3d" | |
| 4229 | + | "signing_secret": "g1ts_9f2c4a7e1b0d3c6f8a2e5b7d9c1f4a6e8b0d2c4f6a8e1b3d" | |
| 3864 | 4230 | } | |
| 3865 | 4231 | } | |
| 3866 | 4232 | } | |
| 4035 | 4401 | "pull.merged" | |
| 4036 | 4402 | ], | |
| 4037 | 4403 | "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" | |
| 4043 | 4409 | } | |
| 4044 | 4410 | ] | |
| 4045 | 4411 | } | |
| 4093 | 4459 | "Webhooks" | |
| 4094 | 4460 | ], | |
| 4095 | 4461 | "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/).", | |
| 4097 | 4463 | "x-mcp-tool": "create_webhook", | |
| 4098 | 4464 | "security": [ | |
| 4099 | 4465 | { | |
| 4138 | 4504 | "pull.merged" | |
| 4139 | 4505 | ], | |
| 4140 | 4506 | "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" | |
| 4146 | 4512 | }, | |
| 4147 | 4513 | "secret": "whsec_…" | |
| 4148 | 4514 | } | |
| 4326 | 4692 | "pull.merged" | |
| 4327 | 4693 | ], | |
| 4328 | 4694 | "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" | |
| 4334 | 4700 | } | |
| 4335 | 4701 | } | |
| 4336 | 4702 | } | |
| 4619 | 4985 | "schema": {}, | |
| 4620 | 4986 | "example": { | |
| 4621 | 4987 | "id": "dlv_01kpx5p9v2j7k3m8n4p5q6r7st", | |
| 4622 | − | "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 4623 | − | "eventId": "", | |
| 4988 | + | "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 4989 | + | "event_id": "", | |
| 4624 | 4990 | "event": "ping", | |
| 4625 | 4991 | "status": "delivered", | |
| 4626 | 4992 | "attempts": 1, | |
| 4627 | − | "responseStatus": 200, | |
| 4628 | − | "responseBody": "ok", | |
| 4993 | + | "response_status": 200, | |
| 4994 | + | "response_body": "ok", | |
| 4629 | 4995 | "error": null, | |
| 4630 | − | "durationMs": 184, | |
| 4996 | + | "duration_ms": 184, | |
| 4631 | 4997 | "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 | |
| 4635 | 5001 | } | |
| 4636 | 5002 | } | |
| 4637 | 5003 | } | |
| 4715 | 5081 | "Webhooks" | |
| 4716 | 5082 | ], | |
| 4717 | 5083 | "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.", | |
| 4719 | 5085 | "x-mcp-tool": "list_webhook_deliveries", | |
| 4720 | 5086 | "security": [ | |
| 4721 | 5087 | { | |
| 4760 | 5126 | "example": [ | |
| 4761 | 5127 | { | |
| 4762 | 5128 | "id": "dlv_01kpx6a2b3c4d5e6f7g8h9j0kz", | |
| 4763 | − | "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 4764 | − | "eventId": "", | |
| 5129 | + | "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 5130 | + | "event_id": "", | |
| 4765 | 5131 | "event": "ping", | |
| 4766 | 5132 | "status": "pending", | |
| 4767 | 5133 | "attempts": 1, | |
| 4768 | − | "responseStatus": 503, | |
| 4769 | − | "responseBody": "Service Unavailable", | |
| 5134 | + | "response_status": 503, | |
| 5135 | + | "response_body": "Service Unavailable", | |
| 4770 | 5136 | "error": null, | |
| 4771 | − | "durationMs": 212, | |
| 5137 | + | "duration_ms": 212, | |
| 4772 | 5138 | "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" | |
| 4776 | 5142 | }, | |
| 4777 | 5143 | { | |
| 4778 | 5144 | "id": "dlv_01kpx5p9v2j7k3m8n4p5q6r7st", | |
| 4779 | − | "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 4780 | − | "eventId": "", | |
| 5145 | + | "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 5146 | + | "event_id": "", | |
| 4781 | 5147 | "event": "ping", | |
| 4782 | 5148 | "status": "delivered", | |
| 4783 | 5149 | "attempts": 1, | |
| 4784 | − | "responseStatus": 200, | |
| 4785 | − | "responseBody": "ok", | |
| 5150 | + | "response_status": 200, | |
| 5151 | + | "response_body": "ok", | |
| 4786 | 5152 | "error": null, | |
| 4787 | − | "durationMs": 184, | |
| 5153 | + | "duration_ms": 184, | |
| 4788 | 5154 | "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 | |
| 4792 | 5158 | } | |
| 4793 | 5159 | ] | |
| 4794 | 5160 | } | |
| 4900 | 5266 | "schema": {}, | |
| 4901 | 5267 | "example": { | |
| 4902 | 5268 | "id": "dlv_01kpx6c8d1e2f3g4h5j6k7m8np", | |
| 4903 | − | "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 4904 | − | "eventId": "", | |
| 5269 | + | "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 5270 | + | "event_id": "", | |
| 4905 | 5271 | "event": "ping", | |
| 4906 | 5272 | "status": "delivered", | |
| 4907 | 5273 | "attempts": 1, | |
| 4908 | − | "responseStatus": 200, | |
| 4909 | − | "responseBody": "ok", | |
| 5274 | + | "response_status": 200, | |
| 5275 | + | "response_body": "ok", | |
| 4910 | 5276 | "error": null, | |
| 4911 | − | "durationMs": 171, | |
| 5277 | + | "duration_ms": 171, | |
| 4912 | 5278 | "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 | |
| 4916 | 5282 | } | |
| 4917 | 5283 | } | |
| 4918 | 5284 | } | |
| 5033 | 5399 | "pull.merged" | |
| 5034 | 5400 | ], | |
| 5035 | 5401 | "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" | |
| 5041 | 5407 | } | |
| 5042 | 5408 | ] | |
| 5043 | 5409 | } | |
| 5094 | 5460 | "Webhooks" | |
| 5095 | 5461 | ], | |
| 5096 | 5462 | "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/).", | |
| 5098 | 5464 | "x-mcp-tool": "create_webhook", | |
| 5099 | 5465 | "security": [ | |
| 5100 | 5466 | { | |
| 5129 | 5495 | "*" | |
| 5130 | 5496 | ], | |
| 5131 | 5497 | "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" | |
| 5137 | 5503 | }, | |
| 5138 | 5504 | "secret": "whsec_…" | |
| 5139 | 5505 | } | |
| 5303 | 5669 | "pull.merged" | |
| 5304 | 5670 | ], | |
| 5305 | 5671 | "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" | |
| 5311 | 5677 | } | |
| 5312 | 5678 | } | |
| 5313 | 5679 | } | |
| 5560 | 5926 | "schema": {}, | |
| 5561 | 5927 | "example": { | |
| 5562 | 5928 | "id": "dlv_01kpx5p9v2j7k3m8n4p5q6r7st", | |
| 5563 | − | "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 5564 | − | "eventId": "", | |
| 5929 | + | "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 5930 | + | "event_id": "", | |
| 5565 | 5931 | "event": "ping", | |
| 5566 | 5932 | "status": "delivered", | |
| 5567 | 5933 | "attempts": 1, | |
| 5568 | − | "responseStatus": 200, | |
| 5569 | − | "responseBody": "ok", | |
| 5934 | + | "response_status": 200, | |
| 5935 | + | "response_body": "ok", | |
| 5570 | 5936 | "error": null, | |
| 5571 | − | "durationMs": 184, | |
| 5937 | + | "duration_ms": 184, | |
| 5572 | 5938 | "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 | |
| 5576 | 5942 | } | |
| 5577 | 5943 | } | |
| 5578 | 5944 | } | |
| 5677 | 6043 | "example": [ | |
| 5678 | 6044 | { | |
| 5679 | 6045 | "id": "dlv_01kpx6a2b3c4d5e6f7g8h9j0kz", | |
| 5680 | − | "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 5681 | − | "eventId": "", | |
| 6046 | + | "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 6047 | + | "event_id": "", | |
| 5682 | 6048 | "event": "ping", | |
| 5683 | 6049 | "status": "pending", | |
| 5684 | 6050 | "attempts": 1, | |
| 5685 | − | "responseStatus": 503, | |
| 5686 | − | "responseBody": "Service Unavailable", | |
| 6051 | + | "response_status": 503, | |
| 6052 | + | "response_body": "Service Unavailable", | |
| 5687 | 6053 | "error": null, | |
| 5688 | − | "durationMs": 212, | |
| 6054 | + | "duration_ms": 212, | |
| 5689 | 6055 | "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" | |
| 5693 | 6059 | }, | |
| 5694 | 6060 | { | |
| 5695 | 6061 | "id": "dlv_01kpx5p9v2j7k3m8n4p5q6r7st", | |
| 5696 | − | "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 5697 | − | "eventId": "", | |
| 6062 | + | "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 6063 | + | "event_id": "", | |
| 5698 | 6064 | "event": "ping", | |
| 5699 | 6065 | "status": "delivered", | |
| 5700 | 6066 | "attempts": 1, | |
| 5701 | − | "responseStatus": 200, | |
| 5702 | − | "responseBody": "ok", | |
| 6067 | + | "response_status": 200, | |
| 6068 | + | "response_body": "ok", | |
| 5703 | 6069 | "error": null, | |
| 5704 | − | "durationMs": 184, | |
| 6070 | + | "duration_ms": 184, | |
| 5705 | 6071 | "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 | |
| 5709 | 6075 | } | |
| 5710 | 6076 | ] | |
| 5711 | 6077 | } | |
| 5809 | 6175 | "schema": {}, | |
| 5810 | 6176 | "example": { | |
| 5811 | 6177 | "id": "dlv_01kpx6c8d1e2f3g4h5j6k7m8np", | |
| 5812 | − | "hookId": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 5813 | − | "eventId": "", | |
| 6178 | + | "hook_id": "hk_01kpx5p9s4h0t1w6x8y2a3b4cd", | |
| 6179 | + | "event_id": "", | |
| 5814 | 6180 | "event": "ping", | |
| 5815 | 6181 | "status": "delivered", | |
| 5816 | 6182 | "attempts": 1, | |
| 5817 | − | "responseStatus": 200, | |
| 5818 | − | "responseBody": "ok", | |
| 6183 | + | "response_status": 200, | |
| 6184 | + | "response_body": "ok", | |
| 5819 | 6185 | "error": null, | |
| 5820 | − | "durationMs": 171, | |
| 6186 | + | "duration_ms": 171, | |
| 5821 | 6187 | "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 | |
| 5825 | 6191 | } | |
| 5826 | 6192 | } | |
| 5827 | 6193 | } | |
| 5918 | 6284 | "example": [ | |
| 5919 | 6285 | { | |
| 5920 | 6286 | "task": "default", | |
| 5921 | − | "connectionId": "con_01kpx3m7q2f8r9t4v6w0y1z2ab", | |
| 6287 | + | "connection_id": "con_01kpx3m7q2f8r9t4v6w0y1z2ab", | |
| 5922 | 6288 | "model": "claude-sonnet-4-5" | |
| 5923 | 6289 | }, | |
| 5924 | 6290 | { | |
| 5925 | 6291 | "task": "review", | |
| 5926 | − | "connectionId": null, | |
| 6292 | + | "connection_id": null, | |
| 5927 | 6293 | "model": null | |
| 5928 | 6294 | } | |
| 5929 | 6295 | ] | |
| 6008 | 6374 | "example": [ | |
| 6009 | 6375 | { | |
| 6010 | 6376 | "task": "default", | |
| 6011 | − | "connectionId": "con_01kpx3m7q2f8r9t4v6w0y1z2ab", | |
| 6377 | + | "connection_id": "con_01kpx3m7q2f8r9t4v6w0y1z2ab", | |
| 6012 | 6378 | "model": "claude-sonnet-4-5" | |
| 6013 | 6379 | }, | |
| 6014 | 6380 | { | |
| 6015 | 6381 | "task": "review", | |
| 6016 | − | "connectionId": null, | |
| 6382 | + | "connection_id": null, | |
| 6017 | 6383 | "model": null | |
| 6018 | 6384 | } | |
| 6019 | 6385 | ] | |
| 6410 | 6776 | "default": false | |
| 6411 | 6777 | } | |
| 6412 | 6778 | }, | |
| 6413 | − | "lastRun": { | |
| 6779 | + | "last_run": { | |
| 6414 | 6780 | "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 6415 | − | "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 6781 | + | "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 6416 | 6782 | "path": ".g1t/workflows/ci.yml", | |
| 6417 | 6783 | "name": "CI", | |
| 6418 | 6784 | "title": "Greeting should name the caller", | |
| 6426 | 6792 | "conclusion": "success", | |
| 6427 | 6793 | "error": null, | |
| 6428 | 6794 | "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" | |
| 6432 | 6798 | } | |
| 6433 | 6799 | }, | |
| 6434 | 6800 | { | |
| 6442 | 6808 | "error": null, | |
| 6443 | 6809 | "notes": [], | |
| 6444 | 6810 | "dispatch": null, | |
| 6445 | − | "lastRun": null | |
| 6811 | + | "last_run": null | |
| 6446 | 6812 | } | |
| 6447 | 6813 | ] | |
| 6448 | 6814 | } | |
| 6569 | 6935 | "example": [ | |
| 6570 | 6936 | { | |
| 6571 | 6937 | "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 6572 | − | "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 6938 | + | "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 6573 | 6939 | "path": ".g1t/workflows/ci.yml", | |
| 6574 | 6940 | "name": "CI", | |
| 6575 | 6941 | "title": "Greeting should name the caller", | |
| 6583 | 6949 | "conclusion": "success", | |
| 6584 | 6950 | "error": null, | |
| 6585 | 6951 | "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" | |
| 6589 | 6955 | } | |
| 6590 | 6956 | ] | |
| 6591 | 6957 | } | |
| 6692 | 7058 | "schema": {}, | |
| 6693 | 7059 | "example": { | |
| 6694 | 7060 | "id": "run_01kpx8d4e7f0g3h6j9k2m5n8pq", | |
| 6695 | − | "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 7061 | + | "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 6696 | 7062 | "path": ".g1t/workflows/ci.yml", | |
| 6697 | 7063 | "name": "CI", | |
| 6698 | 7064 | "title": "CI run by syntaqx", | |
| 6706 | 7072 | "conclusion": null, | |
| 6707 | 7073 | "error": null, | |
| 6708 | 7074 | "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 | |
| 6712 | 7078 | } | |
| 6713 | 7079 | } | |
| 6714 | 7080 | } | |
| 6855 | 7221 | "error": null, | |
| 6856 | 7222 | "notes": [], | |
| 6857 | 7223 | "dispatch": null, | |
| 6858 | − | "lastRun": null | |
| 7224 | + | "last_run": null | |
| 6859 | 7225 | } | |
| 6860 | 7226 | } | |
| 6861 | 7227 | } | |
| 6997 | 7363 | "error": null, | |
| 6998 | 7364 | "notes": [], | |
| 6999 | 7365 | "dispatch": null, | |
| 7000 | − | "lastRun": null | |
| 7366 | + | "last_run": null | |
| 7001 | 7367 | } | |
| 7002 | 7368 | } | |
| 7003 | 7369 | } | |
| 7136 | 7502 | "error": null, | |
| 7137 | 7503 | "notes": [], | |
| 7138 | 7504 | "dispatch": null, | |
| 7139 | − | "lastRun": null | |
| 7505 | + | "last_run": null | |
| 7140 | 7506 | } | |
| 7141 | 7507 | } | |
| 7142 | 7508 | } | |
| 7311 | 7677 | "example": [ | |
| 7312 | 7678 | { | |
| 7313 | 7679 | "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 7314 | − | "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 7680 | + | "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 7315 | 7681 | "path": ".g1t/workflows/ci.yml", | |
| 7316 | 7682 | "name": "CI", | |
| 7317 | 7683 | "title": "Greeting should name the caller", | |
| 7325 | 7691 | "conclusion": "success", | |
| 7326 | 7692 | "error": null, | |
| 7327 | 7693 | "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" | |
| 7331 | 7697 | } | |
| 7332 | 7698 | ] | |
| 7333 | 7699 | } | |
| 7433 | 7799 | "example": { | |
| 7434 | 7800 | "run": { | |
| 7435 | 7801 | "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 7436 | − | "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 7802 | + | "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 7437 | 7803 | "path": ".g1t/workflows/ci.yml", | |
| 7438 | 7804 | "name": "CI", | |
| 7439 | 7805 | "title": "Greeting should name the caller", | |
| 7447 | 7813 | "conclusion": "failure", | |
| 7448 | 7814 | "error": null, | |
| 7449 | 7815 | "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" | |
| 7453 | 7819 | }, | |
| 7454 | 7820 | "jobs": [ | |
| 7455 | 7821 | { | |
| 7456 | 7822 | "id": "job_01kpx7b3d0e4f8g2h6j0k4m8ns", | |
| 7457 | − | "runId": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 7823 | + | "run_id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 7458 | 7824 | "key": "test", | |
| 7459 | 7825 | "name": "test", | |
| 7460 | 7826 | "needs": [], | |
| 7466 | 7832 | "name": "Run actions/checkout@v4", | |
| 7467 | 7833 | "status": "completed", | |
| 7468 | 7834 | "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" | |
| 7471 | 7837 | }, | |
| 7472 | 7838 | { | |
| 7473 | 7839 | "number": 2, | |
| 7474 | 7840 | "name": "Run cargo test", | |
| 7475 | 7841 | "status": "completed", | |
| 7476 | 7842 | "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" | |
| 7479 | 7845 | } | |
| 7480 | 7846 | ], | |
| 7481 | 7847 | "annotations": [ | |
| 7488 | 7854 | } | |
| 7489 | 7855 | ], | |
| 7490 | 7856 | "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" | |
| 7493 | 7859 | }, | |
| 7494 | 7860 | { | |
| 7495 | 7861 | "id": "job_01kpx7b3d0e4f8g2h6j0k4m8nt", | |
| 7496 | − | "runId": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 7862 | + | "run_id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 7497 | 7863 | "key": "deploy", | |
| 7498 | 7864 | "name": "deploy", | |
| 7499 | 7865 | "needs": [ | |
| 7504 | 7870 | "steps": [], | |
| 7505 | 7871 | "annotations": [], | |
| 7506 | 7872 | "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" | |
| 7509 | 7875 | } | |
| 7510 | 7876 | ], | |
| 7511 | 7877 | "notes": [ | |
| 7616 | 7982 | "schema": {}, | |
| 7617 | 7983 | "example": { | |
| 7618 | 7984 | "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 7619 | − | "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 7985 | + | "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 7620 | 7986 | "path": ".g1t/workflows/ci.yml", | |
| 7621 | 7987 | "name": "CI", | |
| 7622 | 7988 | "title": "Greeting should name the caller", | |
| 7630 | 7996 | "conclusion": "cancelled", | |
| 7631 | 7997 | "error": null, | |
| 7632 | 7998 | "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" | |
| 7636 | 8002 | } | |
| 7637 | 8003 | } | |
| 7638 | 8004 | } | |
| 7744 | 8110 | "schema": {}, | |
| 7745 | 8111 | "example": { | |
| 7746 | 8112 | "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 7747 | − | "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 8113 | + | "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 7748 | 8114 | "path": ".g1t/workflows/ci.yml", | |
| 7749 | 8115 | "name": "CI", | |
| 7750 | 8116 | "title": "Greeting should name the caller", | |
| 7758 | 8124 | "conclusion": null, | |
| 7759 | 8125 | "error": null, | |
| 7760 | 8126 | "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 | |
| 7764 | 8130 | } | |
| 7765 | 8131 | } | |
| 7766 | 8132 | } | |
| 7891 | 8257 | "schema": {}, | |
| 7892 | 8258 | "example": { | |
| 7893 | 8259 | "id": "run_01kpx7b3c6d9e2f5g8h1j4k7mr", | |
| 7894 | − | "workflowId": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 8260 | + | "workflow_id": "wfl_01kpw2c5d8e1f4g7h0j3k6m9np", | |
| 7895 | 8261 | "path": ".g1t/workflows/ci.yml", | |
| 7896 | 8262 | "name": "CI", | |
| 7897 | 8263 | "title": "Greeting should name the caller", | |
| 7905 | 8271 | "conclusion": null, | |
| 7906 | 8272 | "error": null, | |
| 7907 | 8273 | "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 | |
| 7911 | 8277 | } | |
| 7912 | 8278 | } | |
| 7913 | 8279 | } | |
| 8156 | 8522 | "kind": "secret", | |
| 8157 | 8523 | "value": null, | |
| 8158 | 8524 | "scope": "workspace", | |
| 8159 | − | "updatedAt": "2026-10-01T09:12:44.020Z", | |
| 8160 | − | "availableTo": [ | |
| 8525 | + | "updated_at": "2026-10-01T09:12:44.020Z", | |
| 8526 | + | "available_to": [ | |
| 8161 | 8527 | "workflows" | |
| 8162 | 8528 | ], | |
| 8163 | 8529 | "environments": [], | |
| 8164 | 8530 | "projects": [], | |
| 8165 | 8531 | "note": null, | |
| 8166 | − | "updatedBy": "syntaqx" | |
| 8532 | + | "updated_by": "syntaqx" | |
| 8167 | 8533 | }, | |
| 8168 | 8534 | { | |
| 8169 | 8535 | "id": "set_01kpv9q4r7s0t3v6w9x2y5z8ac", | |
| 8171 | 8537 | "kind": "secret", | |
| 8172 | 8538 | "value": null, | |
| 8173 | 8539 | "scope": "project", | |
| 8174 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 8175 | − | "availableTo": [ | |
| 8540 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 8541 | + | "available_to": [ | |
| 8176 | 8542 | "workflows", | |
| 8177 | 8543 | "deployments" | |
| 8178 | 8544 | ], | |
| 8181 | 8547 | ], | |
| 8182 | 8548 | "projects": [], | |
| 8183 | 8549 | "note": "Rotate in the Stripe dashboard.", | |
| 8184 | − | "updatedBy": "syntaqx" | |
| 8550 | + | "updated_by": "syntaqx" | |
| 8185 | 8551 | } | |
| 8186 | 8552 | ] | |
| 8187 | 8553 | } | |
| 8285 | 8651 | "kind": "secret", | |
| 8286 | 8652 | "value": null, | |
| 8287 | 8653 | "scope": "project", | |
| 8288 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 8289 | − | "availableTo": [ | |
| 8654 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 8655 | + | "available_to": [ | |
| 8290 | 8656 | "workflows", | |
| 8291 | 8657 | "deployments" | |
| 8292 | 8658 | ], | |
| 8295 | 8661 | ], | |
| 8296 | 8662 | "projects": [], | |
| 8297 | 8663 | "note": null, | |
| 8298 | − | "updatedBy": "syntaqx" | |
| 8664 | + | "updated_by": "syntaqx" | |
| 8299 | 8665 | } | |
| 8300 | 8666 | } | |
| 8301 | 8667 | } | |
| 8593 | 8959 | "kind": "variable", | |
| 8594 | 8960 | "value": "20", | |
| 8595 | 8961 | "scope": "project", | |
| 8596 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 8597 | − | "availableTo": [ | |
| 8962 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 8963 | + | "available_to": [ | |
| 8598 | 8964 | "workflows", | |
| 8599 | 8965 | "deployments" | |
| 8600 | 8966 | ], | |
| 8601 | 8967 | "environments": [], | |
| 8602 | 8968 | "projects": [], | |
| 8603 | 8969 | "note": null, | |
| 8604 | − | "updatedBy": "syntaqx" | |
| 8970 | + | "updated_by": "syntaqx" | |
| 8605 | 8971 | } | |
| 8606 | 8972 | ] | |
| 8607 | 8973 | } | |
| 8694 | 9060 | "kind": "variable", | |
| 8695 | 9061 | "value": "20", | |
| 8696 | 9062 | "scope": "project", | |
| 8697 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 8698 | − | "availableTo": [ | |
| 9063 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 9064 | + | "available_to": [ | |
| 8699 | 9065 | "workflows", | |
| 8700 | 9066 | "deployments" | |
| 8701 | 9067 | ], | |
| 8702 | 9068 | "environments": [], | |
| 8703 | 9069 | "projects": [], | |
| 8704 | 9070 | "note": null, | |
| 8705 | − | "updatedBy": "syntaqx" | |
| 9071 | + | "updated_by": "syntaqx" | |
| 8706 | 9072 | } | |
| 8707 | 9073 | } | |
| 8708 | 9074 | } | |
| 8879 | 9245 | "kind": "variable", | |
| 8880 | 9246 | "value": "22", | |
| 8881 | 9247 | "scope": "project", | |
| 8882 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 8883 | − | "availableTo": [ | |
| 9248 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 9249 | + | "available_to": [ | |
| 8884 | 9250 | "workflows", | |
| 8885 | 9251 | "deployments" | |
| 8886 | 9252 | ], | |
| 8887 | 9253 | "environments": [], | |
| 8888 | 9254 | "projects": [], | |
| 8889 | 9255 | "note": null, | |
| 8890 | − | "updatedBy": "syntaqx" | |
| 9256 | + | "updated_by": "syntaqx" | |
| 8891 | 9257 | } | |
| 8892 | 9258 | } | |
| 8893 | 9259 | } | |
| 9169 | 9535 | "kind": "secret", | |
| 9170 | 9536 | "value": null, | |
| 9171 | 9537 | "scope": "workspace", | |
| 9172 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 9173 | − | "availableTo": [ | |
| 9538 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 9539 | + | "available_to": [ | |
| 9174 | 9540 | "workflows" | |
| 9175 | 9541 | ], | |
| 9176 | 9542 | "environments": [], | |
| 9177 | 9543 | "projects": [], | |
| 9178 | 9544 | "note": null, | |
| 9179 | − | "updatedBy": "syntaqx" | |
| 9545 | + | "updated_by": "syntaqx" | |
| 9180 | 9546 | } | |
| 9181 | 9547 | ] | |
| 9182 | 9548 | } | |
| 9274 | 9640 | "kind": "secret", | |
| 9275 | 9641 | "value": null, | |
| 9276 | 9642 | "scope": "workspace", | |
| 9277 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 9278 | − | "availableTo": [ | |
| 9643 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 9644 | + | "available_to": [ | |
| 9279 | 9645 | "workflows" | |
| 9280 | 9646 | ], | |
| 9281 | 9647 | "environments": [], | |
| 9283 | 9649 | "hello" | |
| 9284 | 9650 | ], | |
| 9285 | 9651 | "note": null, | |
| 9286 | − | "updatedBy": "syntaqx" | |
| 9652 | + | "updated_by": "syntaqx" | |
| 9287 | 9653 | } | |
| 9288 | 9654 | } | |
| 9289 | 9655 | } | |
| 9553 | 9919 | "kind": "variable", | |
| 9554 | 9920 | "value": "20", | |
| 9555 | 9921 | "scope": "workspace", | |
| 9556 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 9557 | − | "availableTo": [ | |
| 9922 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 9923 | + | "available_to": [ | |
| 9558 | 9924 | "workflows", | |
| 9559 | 9925 | "deployments" | |
| 9560 | 9926 | ], | |
| 9561 | 9927 | "environments": [], | |
| 9562 | 9928 | "projects": [], | |
| 9563 | 9929 | "note": null, | |
| 9564 | − | "updatedBy": "syntaqx" | |
| 9930 | + | "updated_by": "syntaqx" | |
| 9565 | 9931 | } | |
| 9566 | 9932 | ] | |
| 9567 | 9933 | } | |
| 9648 | 10014 | "kind": "variable", | |
| 9649 | 10015 | "value": "20", | |
| 9650 | 10016 | "scope": "workspace", | |
| 9651 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 9652 | − | "availableTo": [ | |
| 10017 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 10018 | + | "available_to": [ | |
| 9653 | 10019 | "workflows", | |
| 9654 | 10020 | "deployments" | |
| 9655 | 10021 | ], | |
| 9656 | 10022 | "environments": [], | |
| 9657 | 10023 | "projects": [], | |
| 9658 | 10024 | "note": null, | |
| 9659 | − | "updatedBy": "syntaqx" | |
| 10025 | + | "updated_by": "syntaqx" | |
| 9660 | 10026 | } | |
| 9661 | 10027 | } | |
| 9662 | 10028 | } | |
| 9823 | 10189 | "kind": "variable", | |
| 9824 | 10190 | "value": "22", | |
| 9825 | 10191 | "scope": "workspace", | |
| 9826 | − | "updatedAt": "2026-10-04T15:42:07.318Z", | |
| 9827 | − | "availableTo": [ | |
| 10192 | + | "updated_at": "2026-10-04T15:42:07.318Z", | |
| 10193 | + | "available_to": [ | |
| 9828 | 10194 | "workflows", | |
| 9829 | 10195 | "deployments" | |
| 9830 | 10196 | ], | |
| 9831 | 10197 | "environments": [], | |
| 9832 | 10198 | "projects": [], | |
| 9833 | 10199 | "note": null, | |
| 9834 | − | "updatedBy": "syntaqx" | |
| 10200 | + | "updated_by": "syntaqx" | |
| 9835 | 10201 | } | |
| 9836 | 10202 | } | |
| 9837 | 10203 | } | |
| 10101 | 10467 | "application/json": { | |
| 10102 | 10468 | "schema": {}, | |
| 10103 | 10469 | "example": { | |
| 10104 | − | "planId": "pln_01m43s9c4e8g2j6m0q4t8x2b6d" | |
| 10470 | + | "plan_id": "pln_01m43s9c4e8g2j6m0q4t8x2b6d" | |
| 10105 | 10471 | } | |
| 10106 | 10472 | } | |
| 10107 | 10473 | } | |
| 10242 | 10608 | "schema": {}, | |
| 10243 | 10609 | "example": { | |
| 10244 | 10610 | "id": "pln_01m43s9c4e8g2j6m0q4t8x2b6d", | |
| 10245 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 10611 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 10246 | 10612 | "brief": "Greet people by name, from the command line and the web page.", | |
| 10247 | 10613 | "status": "ready", | |
| 10248 | 10614 | "summary": "Name parsing first, then the two places that print the greeting.", | |
| 10259 | 10625 | "files": [ | |
| 10260 | 10626 | "src/greet.rs" | |
| 10261 | 10627 | ], | |
| 10262 | − | "dependsOn": [], | |
| 10628 | + | "depends_on": [], | |
| 10263 | 10629 | "number": null | |
| 10264 | 10630 | }, | |
| 10265 | 10631 | { | |
| 10274 | 10640 | "files": [ | |
| 10275 | 10641 | "src/web.rs" | |
| 10276 | 10642 | ], | |
| 10277 | − | "dependsOn": [ | |
| 10643 | + | "depends_on": [ | |
| 10278 | 10644 | 1 | |
| 10279 | 10645 | ], | |
| 10280 | 10646 | "number": null | |
| 10288 | 10654 | "verified": false, | |
| 10289 | 10655 | "workspaces": [] | |
| 10290 | 10656 | }, | |
| 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", | |
| 10293 | 10659 | "progress": [], | |
| 10294 | 10660 | "exchanges": [] | |
| 10295 | 10661 | } | |
| 10393 | 10759 | "schema": {}, | |
| 10394 | 10760 | "example": { | |
| 10395 | 10761 | "id": "pln_01m43s9c4e8g2j6m0q4t8x2b6d", | |
| 10396 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 10762 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 10397 | 10763 | "brief": "Greet people by name, from the command line and the web page.", | |
| 10398 | 10764 | "status": "applied", | |
| 10399 | 10765 | "summary": "Name parsing first, then the two places that print the greeting.", | |
| 10410 | 10776 | "files": [ | |
| 10411 | 10777 | "src/greet.rs" | |
| 10412 | 10778 | ], | |
| 10413 | − | "dependsOn": [], | |
| 10779 | + | "depends_on": [], | |
| 10414 | 10780 | "number": 12 | |
| 10415 | 10781 | }, | |
| 10416 | 10782 | { | |
| 10425 | 10791 | "files": [ | |
| 10426 | 10792 | "src/web.rs" | |
| 10427 | 10793 | ], | |
| 10428 | − | "dependsOn": [ | |
| 10794 | + | "depends_on": [ | |
| 10429 | 10795 | 1 | |
| 10430 | 10796 | ], | |
| 10431 | 10797 | "number": 13 | |
| 10439 | 10805 | "verified": false, | |
| 10440 | 10806 | "workspaces": [] | |
| 10441 | 10807 | }, | |
| 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", | |
| 10444 | 10810 | "progress": [], | |
| 10445 | 10811 | "exchanges": [] | |
| 10446 | 10812 | } | |
| 10607 | 10973 | "path": null, | |
| 10608 | 10974 | "line": null, | |
| 10609 | 10975 | "verdict": null, | |
| 10610 | − | "createdAt": "2026-10-01T18:12:30.551Z" | |
| 10976 | + | "created_at": "2026-10-01T18:12:30.551Z" | |
| 10611 | 10977 | } | |
| 10612 | 10978 | } | |
| 10613 | 10979 | } | |
| 10702 | 11068 | "Pull requests" | |
| 10703 | 11069 | ], | |
| 10704 | 11070 | "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.", | |
| 10706 | 11072 | "x-mcp-tool": "list_pull_requests", | |
| 10707 | 11073 | "security": [ | |
| 10708 | 11074 | { | |
| 10751 | 11117 | "example": [ | |
| 10752 | 11118 | { | |
| 10753 | 11119 | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 10754 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 11120 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 10755 | 11121 | "number": 14, | |
| 10756 | 11122 | "issue": 12, | |
| 10757 | 11123 | "title": "Greeting should name the caller", | |
| 10763 | 11129 | "namespace": "pulls", | |
| 10764 | 11130 | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 10765 | 11131 | }, | |
| 10766 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 11132 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 10767 | 11133 | "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", | |
| 10774 | 11140 | "files": [ | |
| 10775 | 11141 | { | |
| 10776 | 11142 | "path": "src/main.rs", | |
| 10789 | 11155 | "verified": false, | |
| 10790 | 11156 | "workspaces": [] | |
| 10791 | 11157 | }, | |
| 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" | |
| 10794 | 11160 | } | |
| 10795 | 11161 | ] | |
| 10796 | 11162 | } | |
| 10883 | 11249 | "example": { | |
| 10884 | 11250 | "pull": { | |
| 10885 | 11251 | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 10886 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 11252 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 10887 | 11253 | "number": 14, | |
| 10888 | 11254 | "issue": 12, | |
| 10889 | 11255 | "title": "Greeting should name the caller", | |
| 10895 | 11261 | "namespace": "pulls", | |
| 10896 | 11262 | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 10897 | 11263 | }, | |
| 10898 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 11264 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 10899 | 11265 | "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, | |
| 10906 | 11272 | "files": [], | |
| 10907 | 11273 | "assignees": [], | |
| 10908 | 11274 | "reviewers": [], | |
| 10913 | 11279 | "verified": false, | |
| 10914 | 11280 | "workspaces": [] | |
| 10915 | 11281 | }, | |
| 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" | |
| 10918 | 11284 | }, | |
| 10919 | 11285 | "git": { | |
| 10920 | 11286 | "remote": "https://g1t.sh/pulls/pr_01m43smh3vexsr5pmp60qwv0vs.git", | |
| 11067 | 11433 | "example": { | |
| 11068 | 11434 | "pull": { | |
| 11069 | 11435 | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 11070 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 11436 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 11071 | 11437 | "number": 14, | |
| 11072 | 11438 | "issue": 12, | |
| 11073 | 11439 | "title": "Greeting should name the caller", | |
| 11079 | 11445 | "namespace": "pulls", | |
| 11080 | 11446 | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 11081 | 11447 | }, | |
| 11082 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 11448 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 11083 | 11449 | "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", | |
| 11090 | 11456 | "files": [ | |
| 11091 | 11457 | { | |
| 11092 | 11458 | "path": "src/main.rs", | |
| 11105 | 11471 | "verified": false, | |
| 11106 | 11472 | "workspaces": [] | |
| 11107 | 11473 | }, | |
| 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" | |
| 11110 | 11476 | }, | |
| 11111 | 11477 | "issue": { | |
| 11112 | 11478 | "id": "iss_01m43shrzpfe49x74ga7sj1c6v", | |
| 11113 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 11479 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 11114 | 11480 | "number": 12, | |
| 11115 | 11481 | "title": "Greeting should name the caller", | |
| 11116 | 11482 | "body": "Take a name from the first argument; fall back to world.", | |
| 11122 | 11488 | ], | |
| 11123 | 11489 | "state": "open", | |
| 11124 | 11490 | "reason": null, | |
| 11125 | − | "resolvedBy": null, | |
| 11491 | + | "resolved_by": null, | |
| 11126 | 11492 | "author": { | |
| 11127 | 11493 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 11128 | 11494 | "username": "syntaqx", | |
| 11130 | 11496 | "verified": false, | |
| 11131 | 11497 | "workspaces": [] | |
| 11132 | 11498 | }, | |
| 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, | |
| 11138 | 11504 | "assignees": [], | |
| 11139 | − | "blockedBy": [], | |
| 11505 | + | "blocked_by": [], | |
| 11140 | 11506 | "queued": false, | |
| 11141 | 11507 | "agent": "claude-code" | |
| 11142 | 11508 | }, | |
| 11155 | 11521 | "path": null, | |
| 11156 | 11522 | "line": null, | |
| 11157 | 11523 | "verdict": null, | |
| 11158 | − | "createdAt": "2026-10-01T18:33:10.420Z" | |
| 11524 | + | "created_at": "2026-10-01T18:33:10.420Z" | |
| 11159 | 11525 | } | |
| 11160 | 11526 | ], | |
| 11161 | 11527 | "checks": { | |
| 11162 | 11528 | "id": "chk_01m43sw8e2g6j0m4q8t2x6a0c4", | |
| 11163 | − | "headCommit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 11529 | + | "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13", | |
| 11164 | 11530 | "status": "passed", | |
| 11165 | 11531 | "results": [ | |
| 11166 | 11532 | { | |
| 11167 | 11533 | "command": "cargo test", | |
| 11168 | 11534 | "passed": true, | |
| 11169 | − | "exitCode": 0, | |
| 11535 | + | "exit_code": 0, | |
| 11170 | 11536 | "output": "test result: ok. 3 passed; 0 failed", | |
| 11171 | − | "durationMs": 18234 | |
| 11537 | + | "duration_ms": 18234 | |
| 11172 | 11538 | } | |
| 11173 | 11539 | ], | |
| 11174 | 11540 | "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" | |
| 11177 | 11543 | }, | |
| 11178 | 11544 | "overlaps": [], | |
| 11179 | 11545 | "behind": false, | |
| 11186 | 11552 | "id": "msg_01m43tx5egeh4t3f9zcnjenqvz", | |
| 11187 | 11553 | "author": "syntaqx", | |
| 11188 | 11554 | "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", | |
| 11191 | 11557 | "kind": "message", | |
| 11192 | − | "fromNumber": null, | |
| 11193 | − | "toNumber": 14, | |
| 11558 | + | "from_number": null, | |
| 11559 | + | "to_number": 14, | |
| 11194 | 11560 | "answer": null, | |
| 11195 | 11561 | "declined": false | |
| 11196 | 11562 | } | |
| 11451 | 11817 | "path": null, | |
| 11452 | 11818 | "line": null, | |
| 11453 | 11819 | "verdict": "request_changes", | |
| 11454 | − | "createdAt": "2026-10-01T18:40:05.019Z" | |
| 11820 | + | "created_at": "2026-10-01T18:40:05.019Z" | |
| 11455 | 11821 | } | |
| 11456 | 11822 | } | |
| 11457 | 11823 | } | |
| 11879 | 12245 | "schema": {}, | |
| 11880 | 12246 | "example": { | |
| 11881 | 12247 | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 11882 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 12248 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 11883 | 12249 | "number": 14, | |
| 11884 | 12250 | "issue": 12, | |
| 11885 | 12251 | "title": "Greeting should name the caller", | |
| 11891 | 12257 | "namespace": "pulls", | |
| 11892 | 12258 | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 11893 | 12259 | }, | |
| 11894 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 12260 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 11895 | 12261 | "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, | |
| 11902 | 12268 | "files": [ | |
| 11903 | 12269 | { | |
| 11904 | 12270 | "path": "src/main.rs", | |
| 11915 | 12281 | "verified": false, | |
| 11916 | 12282 | "workspaces": [] | |
| 11917 | 12283 | }, | |
| 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" | |
| 11920 | 12286 | } | |
| 11921 | 12287 | } | |
| 11922 | 12288 | } | |
| 12047 | 12413 | "schema": {}, | |
| 12048 | 12414 | "example": { | |
| 12049 | 12415 | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 12050 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 12416 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 12051 | 12417 | "number": 14, | |
| 12052 | 12418 | "issue": 12, | |
| 12053 | 12419 | "title": "Greeting should name the caller", | |
| 12059 | 12425 | "namespace": "pulls", | |
| 12060 | 12426 | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 12061 | 12427 | }, | |
| 12062 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 12428 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 12063 | 12429 | "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, | |
| 12070 | 12436 | "files": [ | |
| 12071 | 12437 | { | |
| 12072 | 12438 | "path": "src/main.rs", | |
| 12083 | 12449 | "verified": false, | |
| 12084 | 12450 | "workspaces": [] | |
| 12085 | 12451 | }, | |
| 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" | |
| 12088 | 12454 | } | |
| 12089 | 12455 | } | |
| 12090 | 12456 | } | |
| 12149 | 12515 | "Pull requests" | |
| 12150 | 12516 | ], | |
| 12151 | 12517 | "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.", | |
| 12153 | 12519 | "x-mcp-tool": "merge_pull_request", | |
| 12154 | 12520 | "security": [ | |
| 12155 | 12521 | { | |
| 12193 | 12559 | "schema": {}, | |
| 12194 | 12560 | "example": { | |
| 12195 | 12561 | "id": "pr_01m43smh3vexsr5pmp60qwv0vs", | |
| 12196 | − | "repoId": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 12562 | + | "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr", | |
| 12197 | 12563 | "number": 14, | |
| 12198 | 12564 | "issue": 12, | |
| 12199 | 12565 | "title": "Greeting should name the caller", | |
| 12205 | 12571 | "namespace": "pulls", | |
| 12206 | 12572 | "name": "pr_01m43smh3vexsr5pmp60qwv0vs" | |
| 12207 | 12573 | }, | |
| 12208 | − | "forkRepoId": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 12574 | + | "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e", | |
| 12209 | 12575 | "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", | |
| 12216 | 12582 | "files": [ | |
| 12217 | 12583 | { | |
| 12218 | 12584 | "path": "src/main.rs", | |
| 12231 | 12597 | "verified": false, | |
| 12232 | 12598 | "workspaces": [] | |
| 12233 | 12599 | }, | |
| 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" | |
| 12236 | 12602 | } | |
| 12237 | 12603 | } | |
| 12238 | 12604 | } |
| 116 | 116 | background: var(--g1t-surface); | |
| 117 | 117 | color: var(--g1t-fg); | |
| 118 | 118 | } | |
| 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. */ | |
| 120 | 121 | .sidebar-content a[aria-current='page'], | |
| 121 | 122 | .sidebar-content a[aria-current='page']:hover { | |
| 123 | + | border-radius: 0 0.375rem 0.375rem 0; | |
| 122 | 124 | background: var(--g1t-surface); | |
| 123 | 125 | color: var(--g1t-fg); | |
| 124 | 126 | font-weight: 500; |
| 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 | + | } |
| 19 | 19 | takesMessages, | |
| 20 | 20 | } from "@g1t/contracts"; | |
| 21 | 21 | ||
| 22 | + | import { RunAudit } from "./audit"; | |
| 22 | 23 | import { STAGE_LABEL, StageDots } from "./lifecycle"; | |
| 23 | 24 | import { Avatar, TimeAgo } from "./ui"; | |
| 24 | 25 | import { | |
| 361 | 362 | <Avatar name={current.agent} size={24} /> | |
| 362 | 363 | <p className="text-sm"> | |
| 363 | 364 | <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 | + | )} | |
| 364 | 370 | <span className="text-muted"> | |
| 365 | 371 | {active | |
| 366 | 372 | ? `is ${RUN_KIND_LABEL[current.kind].toLowerCase()}` | |
| 406 | 412 | {member && active && <MessageRun run={current} />} | |
| 407 | 413 | {member && active && <StopRun run={current} />} | |
| 408 | 414 | </div> | |
| 415 | + | {member && <RunAudit owner={owner} repo={repo} runIds={runs.map((run) => run.id)} live={active} />} | |
| 409 | 416 | </section> | |
| 410 | 417 | ); | |
| 411 | 418 | } |
| 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 | + | } |
| 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 | + | } |
| 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 | + | } |
| 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 | + | } |
| 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 | + |
| 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"; | |
| 2 | 2 | ||
| 3 | 3 | import { TabLink } from "./ui"; | |
| 4 | 4 | ||
| 21 | 21 | <TabLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}> | |
| 22 | 22 | Dependencies | |
| 23 | 23 | </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> | |
| 24 | 30 | <TabLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}> | |
| 25 | 31 | Secrets and variables | |
| 26 | 32 | </TabLink> |
| 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 | + | } |
| 36 | 36 | PlayCircle, | |
| 37 | 37 | Bot, | |
| 38 | 38 | Brain, | |
| 39 | + | Network, | |
| 39 | 40 | ShieldCheck, | |
| 40 | 41 | Rocket, | |
| 41 | 42 | X, | |
| 363 | 364 | } | |
| 364 | 365 | ||
| 365 | 366 | /** 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)(\/|$)/; | |
| 367 | 368 | ||
| 368 | 369 | /** | |
| 369 | 370 | * The sidebar's menus are two layers, the way a phone pushes a screen: the | |
| 413 | 414 | <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}> | |
| 414 | 415 | Secrets and variables | |
| 415 | 416 | </SidebarLink> | |
| 417 | + | <SidebarLink to={`/${slug}/-/guardrails`} icon={<ShieldCheck size={15} />}> | |
| 418 | + | Guardrails | |
| 419 | + | </SidebarLink> | |
| 416 | 420 | <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}> | |
| 417 | 421 | Webhooks | |
| 418 | 422 | </SidebarLink> | |
| 423 | + | <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}> | |
| 424 | + | Audit log | |
| 425 | + | </SidebarLink> | |
| 419 | 426 | </SidebarGroup> | |
| 420 | 427 | </nav> | |
| 421 | 428 | ); | |
| 500 | 507 | ) : null} | |
| 501 | 508 | <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."> | |
| 502 | 509 | 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 | |
| 506 | 510 | </SidebarSoonLink> | |
| 511 | + | {repo.member ? ( | |
| 512 | + | <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}> | |
| 513 | + | Security | |
| 514 | + | </SidebarLink> | |
| 515 | + | ) : null} | |
| 507 | 516 | <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do."> | |
| 508 | 517 | Insights | |
| 509 | 518 | </SidebarSoonLink> | |
| 679 | 688 | <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}> | |
| 680 | 689 | Agent fleet | |
| 681 | 690 | </SidebarLink> | |
| 691 | + | <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}> | |
| 692 | + | Context | |
| 693 | + | </SidebarLink> | |
| 682 | 694 | <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}> | |
| 683 | 695 | Memory | |
| 684 | 696 | </SidebarLink> | |
| 697 | + | <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}> | |
| 698 | + | Security | |
| 699 | + | </SidebarLink> | |
| 685 | 700 | {roadmapIn("Workspace").map((item) => ( | |
| 686 | 701 | <SidebarSoonLink | |
| 687 | 702 | key={item.key} |
| 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 | + | } |
| 15 | 15 | ||
| 16 | 16 | import { repoAt } from "../lib/markdown-plugins"; | |
| 17 | 17 | 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"; | |
| 19 | 20 | import { CheckboxOption } from "./ui/checkbox"; | |
| 20 | 21 | ||
| 21 | 22 | /** Hues for the labels every repository starts with. */ | |
| 426 | 427 | </span> | |
| 427 | 428 | <Form method="post" className="min-w-0 grow space-y-2" key={resetKey}> | |
| 428 | 429 | <input type="hidden" name="action" value="comment" /> | |
| 429 | − | <Textarea | |
| 430 | + | <MentionTextarea | |
| 430 | 431 | name="body" | |
| 431 | 432 | rows={3} | |
| 432 | 433 | placeholder={ |
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
This change is too large to show in full.