Skip to content
2,976 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Pull requests from branches1//! The work service: issues, pull requests, comments and sessions.
2//!
3//! Other services reach it over `POST /rpc/<method>`; see
4//! `g1t_contracts::work` for the methods and their arguments. It also
5//! consumes its queue of events from the bus.
6
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar7mod agent_comments;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains8mod authored;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API9mod capture;
Acceptance checks in sandboxes, line comments and review verdicts10mod checks;
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)11mod ghost;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar12mod codeowners;
Merge checks: statuses and check runs on every commit13mod commit_checks;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look14mod compute;
Chat controls, public profiles, shadcn selects, and no Docs tab in a project15mod contributions;
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step16mod confidence;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API17mod guardrails;
Inbox: the events service tells people what needs them as events arrive18mod inbox;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar19mod labels;
Agents as a team: lifecycle, merge queue, billing and a new shell20mod lifecycle;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains21mod memory;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API22mod mentions;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains23mod mergeability;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar24mod milestones;
Agents as a team: lifecycle, merge queue, billing and a new shell25mod plans;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request26mod messages;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily27mod prefetch;
Agents as a team: lifecycle, merge queue, billing and a new shell28mod queue;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look29mod retired;
Agents as a team: lifecycle, merge queue, billing and a new shell30mod reviews;
Pull requests from branches31mod rows;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge32mod rulesets;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains33mod runs;
Agents as a team: lifecycle, merge queue, billing and a new shell34mod settings;
GitHub Actions on g1t, part two: running workflows35mod statuses;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar36mod team_reviews;
Pull requests from branches37
38use g1t_contracts::events::{
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts39 ChangedFrom, CommentChanges, CommentCreated, CommentDeleted, CommentEdited, DeletedComment, Event, IssueEvent,
40 NewEvent, Publish, PullEvent, SessionAppended,
Pull requests from branches41};
Agents as a team: lifecycle, merge queue, billing and a new shell42use g1t_contracts::identity::UsernameArgs;
Catching up with main takes seconds when the two sides touched different files43use g1t_contracts::repos::{
Fast pages, required checks on the branch, self-hosted runners, honest incidents44 ForkArgs, GetArgs, HeadArgs, LandArgs, Landed, NeedsAgentReason, PullBranchUpdate, ReadableArgs, Repo, RepoPath,
Catching up with main takes seconds when the two sides touched different files45 UpdatePullBranchArgs,
46};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look47use g1t_contracts::access::{self, Capability, Denied};
Pull requests from branches48use g1t_contracts::time::rfc3339;
49use g1t_contracts::work::*;
Agents as a team: lifecycle, merge queue, billing and a new shell50use futures_util::future::{try_join, try_join3, try_join_all};
Pull requests from branches51use g1t_contracts::{FailureCode, Outcome, User, Viewer, new_id};
Events service in Rust, with RFC 3339 times and accurate push events52use g1t_kit::{args, now_ms, reply, rpc_method};
Pull requests from branches53use serde::Serialize;
54use worker::wasm_bindgen::JsValue;
55use worker::{
56 Context, D1Database, Env, Fetcher, MessageBatch, MessageExt, Request, Response, Result, event,
57};
58
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look59use retired::writable;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar60use rows::{CommentRow, IssueRow, MovedRow, NumberRow, PULL_COLUMNS, PullRow, SessionRow, Snapshot};
Pull requests from branches61
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge62pub(crate) const SOURCE: &str = "work";
Pull requests from branches63const MAX_ENTRY_BATCH: usize = 200;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar64pub(crate) const MAX_ENTRY_CHARS: usize = 64_000;
Pull requests from branches65const MAX_TITLE_CHARS: usize = 200;
66const SESSION_PAGE: u32 = 500;
67const LIST_PAGE: u32 = 100;
Agents as a team: lifecycle, merge queue, billing and a new shell68const MAX_ASSIGNEES: usize = 10;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge69pub(crate) const UNVERIFIED: &str = "Confirm your email address first. Check your inbox, or resend the link from the banner on g1t.sh.";
Pull requests from branches70
71const ISSUE_COLUMNS: &str = "issues.*,
72 (SELECT count(*) FROM pulls WHERE pulls.issue_id = issues.id) AS pull_count,
Agents as a team: lifecycle, merge queue, billing and a new shell73 (SELECT agent FROM pulls
74 WHERE pulls.issue_id = issues.id AND pulls.status IN ('draft', 'open')
75 AND pulls.fork_repo_id IS NOT NULL
76 ORDER BY pulls.number DESC LIMIT 1) AS agent,
Pull requests from branches77 (SELECT count(*) FROM comments
Agents as a team: lifecycle, merge queue, billing and a new shell78 WHERE comments.repo_id = issues.repo_id AND comments.number = issues.number
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar79 AND comments.kind = 'comment') AS comment_count,
80 (SELECT title FROM milestones
81 WHERE milestones.repo_id = issues.repo_id AND milestones.number = issues.milestone) AS milestone_title";
Pull requests from branches82
83fn no_issue<T>() -> Outcome<T> {
84 Outcome::fail(FailureCode::NotFound, "Issue not found.")
85}
86
87fn no_pull<T>() -> Outcome<T> {
88 Outcome::fail(FailureCode::NotFound, "Pull request not found.")
89}
90
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily91/// Whether a pull request was behind when its mergeability was last
92/// worked out, and for which pair of commits (mergeability.rs).
93#[derive(serde::Deserialize)]
94struct StoredBehind {
95 #[serde(default)]
96 behind: Option<u8>,
97 #[serde(default)]
98 mergeable_key: Option<String>,
99}
100
101impl StoredBehind {
102 /// The stored answer, if it was worked out for `head`.
103 fn for_head(&self, head: Option<&str>) -> Option<bool> {
104 let (worked_for, _) = self.mergeable_key.as_deref()?.split_once("..")?;
105 (Some(worked_for) == head).then_some(self.behind? != 0)
106 }
107}
108
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look109/// Refuses `actor` unless their role on `repo` has `capability`: not found
110/// when they cannot read it, forbidden with the role it needs otherwise.
111pub(crate) fn allowed(actor: Option<&User>, repo: &Repo, capability: Capability) -> Outcome<()> {
112 match access::check(actor, repo, capability) {
113 Ok(()) => Outcome::Ok(()),
114 Err(Denied::NotFound) => Outcome::fail(FailureCode::NotFound, "Repository not found."),
115 Err(Denied::Forbidden) => Outcome::fail(
116 FailureCode::Forbidden,
117 access::needs(capability, &format!("{}/{}", repo.namespace, repo.name)),
118 ),
119 }
120}
121
Pull requests from branches122fn optional(value: &Option<String>) -> JsValue {
123 value.as_deref().map_or(JsValue::NULL, JsValue::from)
124}
125
126fn optional_number(value: Option<u32>) -> JsValue {
127 value.map_or(JsValue::NULL, JsValue::from)
128}
129
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar130/// Names the branch a pull request merges into when it is the default
131/// branch, which is stored as none so that it follows a change of default.
132pub(crate) fn fill_base(pull: &mut Pull, repo: &Repo) {
133 if pull.base.as_deref().is_none_or(str::is_empty) {
134 pull.base = Some(repo.default_branch.clone());
135 }
136}
137
138/// The branch a pull request is stored as merging into: none for the
139/// default branch.
140fn stored_base(base: &str, repo: &Repo) -> Option<String> {
141 let base = base.trim();
142 (!base.is_empty() && base != repo.default_branch).then(|| base.to_owned())
143}
144
Pull requests from branches145/// The lowercase name a `State` is stored and sent as.
146fn state_name(state: Option<State>) -> Option<&'static str> {
147 state.map(|state| match state {
148 State::Open => "open",
149 State::Closed => "closed",
150 })
151}
152
153/// A trimmed title, or why it cannot be used.
154fn valid_title(title: &str) -> std::result::Result<&str, &'static str> {
155 let title = title.trim();
156 if title.is_empty() {
157 Err("A title is required.")
158 } else if title.chars().count() > MAX_TITLE_CHARS {
159 Err("That title is too long.")
160 } else {
161 Ok(title)
162 }
163}
164
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts165/// What a closed pull request was when it was closed.
166#[derive(serde::Deserialize)]
167struct ClosedFrom {
168 #[serde(default)]
169 closed_from: Option<String>,
170}
171
172/// Why a pull request in `status` cannot be reopened, if it cannot: only a
173/// closed one can, never a merged one.
174fn reopen_refusal(status: PullStatus) -> Option<&'static str> {
175 match status {
176 PullStatus::Closed => None,
177 PullStatus::Merged => Some("This pull request was merged; it cannot be reopened."),
178 PullStatus::Draft | PullStatus::Open => Some("This pull request is already open."),
179 }
180}
181
182/// What a closed pull request is reopened as: the draft it was, when it
183/// was closed as one, else ready for review.
184fn reopened_status(closed_from: Option<&str>) -> PullStatus {
185 if closed_from == Some("draft") { PullStatus::Draft } else { PullStatus::Open }
186}
187
188/// Why a pull request in `status` cannot be turned into a draft, if it
189/// cannot: only one that is open, ready for review, can.
190fn draft_refusal(status: PullStatus) -> Option<&'static str> {
191 match status {
192 PullStatus::Open => None,
193 PullStatus::Draft => Some("This pull request is already a draft."),
194 PullStatus::Merged => Some("This pull request is already merged."),
195 PullStatus::Closed => Some("This pull request is closed. Reopen it first."),
196 }
197}
198
Pull requests from branches199/// Unwraps an `Outcome`, returning its failure from the enclosing method.
200macro_rules! check {
201 ($outcome:expr) => {
202 match $outcome {
203 Outcome::Ok(value) => value,
204 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
205 }
206 };
207}
208
209struct Work {
210 db: D1Database,
Agents as a team: lifecycle, merge queue, billing and a new shell211 identity: Fetcher,
Pull requests from branches212 repos: Fetcher,
Events service in Rust, with RFC 3339 times and accurate push events213 events: Fetcher,
Sidebar: the panels really slide214 /// GitHub Actions: runs a merge queue's `merge_group` workflows.
215 actions: Fetcher,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily216 /// Where this request's time went, for its `Server-Timing`.
217 timing: g1t_kit::d1::Timing,
218 /// A pull request's rows read in one batch for this request
219 /// (prefetch.rs), which the helpers below read instead of the database.
220 prefetched: std::cell::RefCell<Option<std::rc::Rc<prefetch::Prefetched>>>,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge221 /// Repositories read in this request, by id, for rules that need one
222 /// where only a pull request is at hand (rulesets.rs `repo_for`).
223 known_repos: std::cell::RefCell<std::collections::HashMap<String, Repo>>,
Pull requests from branches224}
225
226impl Work {
227 async fn publish<T: Serialize>(
228 &self,
229 kind: &'static str,
230 repo_id: &str,
231 actor: &User,
232 data: T,
233 ) -> Result<()> {
Merge branch 'worktree-agent-a3abfcce648e87dca'234 // What a workflow job's token did is marked, so it starts no
235 // workflows (`g1t_contracts::events::CAUSED_BY_JOB`).
236 self.publish_as(kind, repo_id, Some(actor.id.clone()), g1t_contracts::events::marked(data, Some(actor)))
Acceptance checks in sandboxes, line comments and review verdicts237 .await
238 }
239
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights240 /// A pull request's owner (whoever asked g1t for it, or its author) as
241 /// a viewer who can read its repository and source. Stored people carry
242 /// no memberships, so a private repository would otherwise look missing
243 /// to them. The membership given reads and nothing more: it is for
244 /// looking, never for acting.
245 pub(crate) async fn owner_viewer(&self, pull: &Pull) -> Result<Viewer> {
Agents move along on private repositories too246 let path: Option<RepoPath> = g1t_kit::call(
247 &self.repos,
248 "path_by_id",
249 &g1t_contracts::repos::PathByIdArgs { id: pull.repo_id.clone() },
250 )
251 .await?;
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights252 let mut owner = pull.owner().clone();
Agents move along on private repositories too253 if let Some(path) = path
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights254 && !owner.is_member(&path.namespace.to_lowercase())
Agents move along on private repositories too255 {
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights256 owner.workspaces.push(g1t_contracts::Membership {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look257 base_permission: Some(access::BasePermission::Read),
258 ..g1t_contracts::Membership::member(path.namespace.to_lowercase())
259 });
Agents move along on private repositories too260 }
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights261 Ok(Some(owner))
Agents move along on private repositories too262 }
263
Acceptance checks in sandboxes, line comments and review verdicts264 /// Publishes an event caused by `actor`, or by g1t itself.
265 async fn publish_as<T: Serialize>(
266 &self,
267 kind: &'static str,
268 repo_id: &str,
269 actor: Option<String>,
270 data: T,
271 ) -> Result<()> {
Pull requests from branches272 let event = NewEvent {
273 kind,
274 source: SOURCE,
275 repo_id: Some(repo_id.to_owned()),
Acceptance checks in sandboxes, line comments and review verdicts276 actor,
Pull requests from branches277 data,
278 };
Events service in Rust, with RFC 3339 times and accurate push events279 g1t_kit::call(
280 &self.events,
281 "publish",
282 &Publish {
283 events: vec![event],
284 },
285 )
286 .await
Pull requests from branches287 }
288
289 /// The repository, if the viewer may see it. Whether they may is
290 /// decided by the repos service.
291 async fn repo(&self, path: &RepoPath, viewer: &Viewer) -> Result<Outcome<Repo>> {
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge292 let found: Outcome<Repo> = self
293 .timing
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily294 .rpc(g1t_kit::call(
295 &self.repos,
296 "get",
297 &GetArgs {
298 path: path.clone(),
299 viewer: viewer.clone(),
300 },
301 ))
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge302 .await?;
303 if let Outcome::Ok(repo) = &found {
304 self.known_repos.borrow_mut().insert(repo.id.clone(), repo.clone());
305 }
306 Ok(found)
Pull requests from branches307 }
308
309 /// The next number in the repository's sequence. Taking it is one
310 /// statement, so concurrent opens cannot be given the same number.
311 async fn next_number(&self, repo_id: &str) -> Result<u32> {
312 let row = self
313 .db
314 .prepare(
315 "INSERT INTO counters (repo_id, last) VALUES (?, 1)
316 ON CONFLICT (repo_id) DO UPDATE SET last = last + 1
317 RETURNING last AS n",
318 )
319 .bind(&[repo_id.into()])?
320 .first::<NumberRow>(None)
321 .await?;
322 row.map(|row| row.n)
323 .ok_or_else(|| worker::Error::RustError("no number was assigned".into()))
324 }
325
326 async fn issue(&self, repo_id: &str, number: u32) -> Result<Option<Issue>> {
327 Ok(self
328 .db
329 .prepare(format!(
330 "SELECT {ISSUE_COLUMNS} FROM issues WHERE repo_id = ? AND number = ?"
331 ))
332 .bind(&[repo_id.into(), number.into()])?
333 .first::<IssueRow>(None)
334 .await?
335 .map(Issue::from))
336 }
337
338 async fn pull(&self, repo_id: &str, number: u32) -> Result<Option<Pull>> {
339 Ok(self
340 .db
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step341 .prepare(format!("SELECT {PULL_COLUMNS} FROM pulls WHERE repo_id = ? AND number = ?"))
Pull requests from branches342 .bind(&[repo_id.into(), number.into()])?
343 .first::<PullRow>(None)
344 .await?
345 .map(Pull::from))
346 }
347
348 /// The repository and one of its issues, as seen by `viewer`.
349 async fn issue_at(
350 &self,
351 path: &RepoPath,
352 number: u32,
353 viewer: &Viewer,
354 ) -> Result<Outcome<(Repo, Issue)>> {
355 let Outcome::Ok(repo) = self.repo(path, viewer).await? else {
356 return Ok(no_issue());
357 };
358 Ok(match self.issue(&repo.id, number).await? {
359 Some(issue) => Outcome::Ok((repo, issue)),
360 None => no_issue(),
361 })
362 }
363
364 /// The repository and one of its pull requests, as seen by `viewer`.
365 async fn pull_at(
366 &self,
367 path: &RepoPath,
368 number: u32,
369 viewer: &Viewer,
370 ) -> Result<Outcome<(Repo, Pull)>> {
371 let Outcome::Ok(repo) = self.repo(path, viewer).await? else {
372 return Ok(no_pull());
373 };
374 Ok(match self.pull(&repo.id, number).await? {
375 Some(pull) => Outcome::Ok((repo, pull)),
376 None => no_pull(),
377 })
378 }
379
Agents as a team: lifecycle, merge queue, billing and a new shell380 /// Records something that happened to an issue or a pull request, so
381 /// that it shows in the conversation where it happened. `text` is what
382 /// `author` did, as the rest of a sentence starting with their name.
383 pub(crate) async fn note(
384 &self,
385 repo_id: &str,
386 number: u32,
387 author: (&str, &str),
388 text: &str,
389 ) -> Result<()> {
390 let now = now_ms();
391 self.db
392 .prepare(
393 "INSERT INTO comments
394 (id, repo_id, number, author_id, author_name, body, kind, created_at)
395 VALUES (?, ?, ?, ?, ?, ?, 'event', ?)",
396 )
397 .bind(&[
398 new_id("cmt", now).into(),
399 repo_id.into(),
400 number.into(),
401 author.0.into(),
402 author.1.into(),
403 text.into(),
404 rfc3339(now).into(),
405 ])?
406 .run()
407 .await?;
408 Ok(())
409 }
410
411 /// Notes who was added to and removed from a list of people, such as
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent412 /// "assigned ana" or "requested a review from g1t".
Agents as a team: lifecycle, merge queue, billing and a new shell413 async fn note_changes(
414 &self,
415 repo_id: &str,
416 number: u32,
417 actor: &User,
418 before: &[String],
419 after: &[String],
420 (added, removed): (&str, &str),
421 ) -> Result<()> {
422 let joined = |names: Vec<&String>| {
423 names
424 .into_iter()
425 .map(String::as_str)
426 .collect::<Vec<_>>()
427 .join(", ")
428 };
429 let new: Vec<&String> = after.iter().filter(|name| !before.contains(name)).collect();
430 let gone: Vec<&String> = before.iter().filter(|name| !after.contains(name)).collect();
431 let who = (actor.id.as_str(), actor.username.as_str());
432 if !new.is_empty() {
433 // Taking something on oneself reads better said that way.
434 let text = if added == "assigned" && new == [&actor.username] {
435 "self-assigned this".to_owned()
436 } else {
437 format!("{added} {}", joined(new))
438 };
439 self.note(repo_id, number, who, &text).await?;
440 }
441 if !gone.is_empty() {
442 self.note(repo_id, number, who, &format!("{removed} {}", joined(gone)))
443 .await?;
444 }
445 Ok(())
446 }
447
Pull requests from branches448 fn issue_event(issue: &Issue) -> IssueEvent {
449 IssueEvent {
450 issue_id: issue.id.clone(),
451 repo_id: issue.repo_id.clone(),
452 number: issue.number,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights453 author: Some((&issue.author).into()),
454 requested_by: issue.requested_by.as_ref().map(Into::into),
Pull requests from branches455 ..IssueEvent::default()
456 }
457 }
458
Workflows run when an agent's pull request is marked ready459 /// The commit a pull request's change is at in git right now: its
460 /// fork's default branch, or its branch.
461 async fn live_head(&self, pull: &Pull) -> Result<Option<String>> {
462 g1t_kit::call(
463 &self.repos,
464 "head",
465 &HeadArgs {
466 repo_id: pull.fork_repo_id.clone().unwrap_or_else(|| pull.repo_id.clone()),
467 branch: pull.branch.clone().unwrap_or_default(),
468 },
469 )
470 .await
471 }
472
Pull requests from branches473 fn pull_event(pull: &Pull) -> PullEvent {
474 PullEvent {
475 pull_id: pull.id.clone(),
476 repo_id: pull.repo_id.clone(),
477 number: pull.number,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights478 author: Some((&pull.author).into()),
479 requested_by: pull.requested_by.as_ref().map(Into::into),
Pull requests from branches480 issue: pull.issue,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step481 confidence: pull.confidence.clone(),
Pull requests from branches482 ..PullEvent::default()
483 }
484 }
485
486 // --- Issues ------------------------------------------------------------
487
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent488 /// Opens an issue for g1t to take at once: refused before
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step489 /// anything is opened unless the actor may put agents to work here. The
490 /// runner's `delegate` starts the agent on it.
491 async fn delegate_issue(&self, a: DelegateIssueArgs) -> Result<Outcome<Issue>> {
492 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
493 check!(writable(&repo));
494 check!(allowed(Some(&a.actor), &repo, Capability::Run));
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails495 if let Some(refused) = mentions::refuse_job_token(&a.actor) {
496 return Ok(refused);
497 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step498 self.open_issue(OpenIssueArgs {
499 actor: a.actor,
500 repo: a.repo,
501 title: a.title,
502 body: a.body,
503 labels: a.labels,
504 checks: a.checks,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar505 milestone: None,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step506 })
507 .await
508 }
509
Pull requests from branches510 async fn open_issue(&self, a: OpenIssueArgs) -> Result<Outcome<Issue>> {
511 if !a.actor.verified {
512 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
513 }
514 let title = match valid_title(&a.title) {
515 Ok(title) => title,
516 Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)),
517 };
518 let Some(labels) = normalize_labels(&a.labels) else {
519 return Ok(Outcome::fail(
520 FailureCode::Invalid,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar521 format!("An issue can have up to {MAX_LABELS} labels of up to {MAX_LABEL_CHARS} characters each."),
Pull requests from branches522 ));
523 };
524 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look525 check!(writable(&repo));
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar526 // Labels it does not have yet are made for someone who may triage;
527 // anyone else picks from those there are.
528 let colors = check!(self.ensure_labels(&a.actor, &repo, &labels).await?);
529 let milestone = match a.milestone.filter(|number| *number > 0) {
530 Some(number) => {
531 check!(allowed(Some(&a.actor), &repo, Capability::Triage));
532 check!(self.milestone_ref(&repo.id, number).await?)
533 }
534 None => None,
535 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents536 // Commands given the old way are words for the agent now: added to
537 // the body under "Definition of done". What has to pass to merge is
538 // the branch's required checks.
539 let body = with_definition_of_done(&a.body, &commands_pass(&a.checks));
Pull requests from branches540
541 let now = now_ms();
542 let id = new_id("iss", now);
543 let number = self.next_number(&repo.id).await?;
544 let timestamp = rfc3339(now);
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights545 // What g1t's agent files at work is g1t's, for the person it works for.
546 let (author, requested_by) = authorship(&a.actor, false);
Pull requests from branches547 self.db
548 .prepare(
549 "INSERT INTO issues
550 (id, repo_id, number, title, body, labels, checks, author_id, author_name,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar551 requested_by_id, requested_by_name, created_at, updated_at, milestone)
552 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Pull requests from branches553 )
554 .bind(&[
555 id.as_str().into(),
556 repo.id.as_str().into(),
557 number.into(),
558 title.into(),
Fast pages, required checks on the branch, self-hosted runners, honest incidents559 body.into(),
Pull requests from branches560 serde_json::to_string(&labels)?.into(),
Fast pages, required checks on the branch, self-hosted runners, honest incidents561 "[]".into(),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights562 author.id.as_str().into(),
563 author.username.as_str().into(),
564 optional(&requested_by.as_ref().map(|user| user.id.clone())),
565 optional(&requested_by.as_ref().map(|user| user.username.clone())),
Pull requests from branches566 timestamp.as_str().into(),
567 timestamp.as_str().into(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar568 optional_number(milestone.as_ref().map(|milestone| milestone.number)),
Pull requests from branches569 ])?
570 .run()
571 .await?;
572 let Some(issue) = self.issue(&repo.id, number).await? else {
573 return Ok(no_issue());
574 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API575 self.apply_label_rule(&a.actor, &issue, &[]).await?;
Pull requests from branches576 self.publish(
577 "issue.opened",
578 &repo.id,
579 &a.actor,
580 IssueEvent {
581 title: Some(issue.title.clone()),
582 ..Self::issue_event(&issue)
583 },
584 )
585 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar586 // Opened with labels and a milestone: each is said, as it would be
587 // if they were added afterwards, without notes in the conversation.
588 for label in &issue.labels {
589 let color = colors.iter().find(|(name, _)| name == label).map_or_else(|| label_color_for(label), |(_, c)| c.clone());
590 let label = Some(g1t_contracts::events::EventLabel { name: label.clone(), color });
591 self.publish("issue.labeled", &repo.id, &a.actor, IssueEvent { label, ..Self::issue_event(&issue) })
592 .await?;
593 }
594 if let Some(milestone) = milestone {
595 self.publish(
596 "issue.milestoned",
597 &repo.id,
598 &a.actor,
599 IssueEvent { milestone: Some(milestone), ..Self::issue_event(&issue) },
600 )
601 .await?;
602 }
Pull requests from branches603 Ok(Outcome::Ok(issue))
604 }
605
606 async fn list_issues(&self, a: ListIssuesArgs) -> Result<Outcome<Vec<Issue>>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily607 let state = state_name(a.state);
Pull requests from branches608 let label = a
609 .label
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar610 .map(|label| label.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase())
Pull requests from branches611 .filter(|label| !label.is_empty());
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar612 let milestone = a.milestone;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily613 let list = |repo_id: String| {
614 let label = label.clone();
615 async move {
616 let state = state.map_or(JsValue::NULL, JsValue::from);
617 let query = self
618 .db
619 .prepare(format!(
620 "SELECT {ISSUE_COLUMNS} FROM issues
621 WHERE repo_id = ? AND (? IS NULL OR state = ?)
622 AND (? IS NULL OR EXISTS
623 (SELECT 1 FROM json_each(issues.labels) WHERE json_each.value = ?))
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar624 AND (? IS NULL OR milestone = ?)
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily625 ORDER BY number DESC LIMIT ?"
626 ))
627 .bind(&[
628 repo_id.into(),
629 state.clone(),
630 state,
631 optional(&label),
632 optional(&label),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar633 optional_number(milestone),
634 optional_number(milestone),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily635 LIST_PAGE.into(),
636 ])?;
637 self.timing.db(1, query.all()).await?.results::<IssueRow>()
638 }
639 };
640 let (_, rows) = check!(self.repo_then(&a.repo, &a.viewer, list).await?);
Pull requests from branches641 Ok(Outcome::Ok(rows.into_iter().map(Issue::from).collect()))
642 }
643
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily644 /// An issue, the pull requests for it and its comments: one batch,
645 /// started beside the access check (prefetch.rs).
Pull requests from branches646 async fn get_issue(&self, a: ViewArgs) -> Result<Outcome<IssueDetail>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily647 let number = a.number;
648 let read = |repo_id: String| async move {
649 let key = [JsValue::from(repo_id.as_str()), JsValue::from(number)];
650 let statements = vec![
651 self.db
652 .prepare(format!("SELECT {ISSUE_COLUMNS} FROM issues WHERE repo_id = ?1 AND number = ?2"))
653 .bind(&key)?,
654 self.db
655 .prepare(format!(
656 "SELECT {PULL_COLUMNS} FROM pulls
657 WHERE issue_id = (SELECT id FROM issues WHERE repo_id = ?1 AND number = ?2)
658 ORDER BY number"
659 ))
660 .bind(&key)?,
661 self.db
662 .prepare("SELECT * FROM comments WHERE repo_id = ?1 AND number = ?2 ORDER BY id LIMIT 500")
663 .bind(&key)?,
664 ];
665 let results = self.timing.db(3, self.db.batch(statements)).await?;
666 let rows = |index: usize| results.get(index).ok_or_else(|| worker::Error::RustError("short batch".into()));
667 Ok::<_, worker::Error>((
668 rows(0)?.results::<IssueRow>()?.into_iter().next().map(Issue::from),
669 rows(1)?.results::<PullRow>()?.into_iter().map(Pull::from).collect::<Vec<_>>(),
670 rows(2)?.results::<CommentRow>()?.into_iter().map(Comment::from).collect::<Vec<_>>(),
671 ))
672 };
673 let Outcome::Ok((_, (Some(issue), pulls, comments))) = self.repo_then(&a.repo, &a.viewer, read).await? else {
674 return Ok(no_issue());
675 };
676 Ok(Outcome::Ok(IssueDetail { comments, pulls, issue }))
Pull requests from branches677 }
678
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look679 /// The issue, if `actor` wrote it or may triage the repository's issues.
Pull requests from branches680 async fn manageable_issue(
681 &self,
682 actor: &User,
683 path: &RepoPath,
684 number: u32,
685 ) -> Result<Outcome<Issue>> {
686 let (repo, issue) = check!(self.issue_at(path, number, &Some(actor.clone())).await?);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look687 check!(writable(&repo));
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights688 if issue.owner().id != actor.id {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look689 check!(allowed(Some(actor), &repo, Capability::Triage));
Pull requests from branches690 }
691 Ok(Outcome::Ok(issue))
692 }
693
694 async fn update_issue(&self, a: UpdateIssueArgs) -> Result<Outcome<Issue>> {
695 let issue = check!(self.manageable_issue(&a.actor, &a.repo, a.number).await?);
696 let title = match a.title.as_deref().map(valid_title) {
697 Some(Err(message)) => return Ok(Outcome::fail(FailureCode::Invalid, message)),
698 Some(Ok(title)) => Some(title.to_owned()),
699 None => None,
700 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar701 if a.labels.as_deref().is_some_and(|labels| normalize_labels(labels).is_none()) {
702 return Ok(Outcome::fail(
703 FailureCode::Invalid,
704 format!("An issue can have up to {MAX_LABELS} labels of up to {MAX_LABEL_CHARS} characters each."),
705 ));
706 }
Agents as a team: lifecycle, merge queue, billing and a new shell707 let assignees = match a.assignees {
708 Some(names) => Some(check!(self.valid_assignees(names).await?)),
709 None => None,
710 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar711 // Its labels and milestone first: either can be refused, and then
712 // nothing else changes.
713 if a.milestone.is_some() || a.labels.is_some() {
714 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
715 if let Some(number) = a.milestone {
716 check!(self.set_milestone(&a.actor, &repo, &labels::Item::Issue(issue.clone()), number).await?);
717 }
718 if let Some(labels) = &a.labels {
719 check!(self.relabel(&a.actor, &repo, &labels::Item::Issue(issue.clone()), labels).await?);
720 }
721 }
Agents as a team: lifecycle, merge queue, billing and a new shell722 let assigned = assignees.as_ref().map(serde_json::to_string).transpose()?;
Pull requests from branches723 let body = a.body.map(|body| body.trim().to_owned());
724 self.db
725 .prepare(
726 "UPDATE issues
727 SET title = COALESCE(?, title), body = COALESCE(?, body),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar728 assignees = COALESCE(?, assignees), updated_at = ?
Pull requests from branches729 WHERE id = ?",
730 )
731 .bind(&[
732 optional(&title),
733 optional(&body),
Agents as a team: lifecycle, merge queue, billing and a new shell734 optional(&assigned),
Pull requests from branches735 rfc3339(now_ms()).into(),
736 issue.id.as_str().into(),
737 ])?
738 .run()
739 .await?;
Agents as a team: lifecycle, merge queue, billing and a new shell740 let before = issue.assignees.clone();
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API741 let labels_before = issue.labels.clone();
Pull requests from branches742 let Some(issue) = self.issue(&issue.repo_id, issue.number).await? else {
743 return Ok(no_issue());
744 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API745 self.apply_label_rule(&a.actor, &issue, &labels_before).await?;
Pull requests from branches746 self.publish(
747 "issue.updated",
748 &issue.repo_id,
749 &a.actor,
750 Self::issue_event(&issue),
751 )
752 .await?;
Agents as a team: lifecycle, merge queue, billing and a new shell753 if let Some(assignees) = assignees {
754 self.note_changes(
755 &issue.repo_id,
756 issue.number,
757 &a.actor,
758 &before,
759 &assignees,
760 ("assigned", "unassigned"),
761 )
762 .await?;
Events: review requests, assignments, stops and deployments are published763 let added: Vec<String> = assignees.iter().filter(|name| !before.contains(name)).cloned().collect();
Agents as a team: lifecycle, merge queue, billing and a new shell764 self.publish(
765 "issue.assigned",
766 &issue.repo_id,
767 &a.actor,
768 IssueEvent {
769 assignees: Some(assignees),
Events: review requests, assignments, stops and deployments are published770 added: Some(added),
Agents as a team: lifecycle, merge queue, billing and a new shell771 ..Self::issue_event(&issue)
772 },
773 )
774 .await?;
775 }
Pull requests from branches776 Ok(Outcome::Ok(issue))
777 }
778
Agents as a team: lifecycle, merge queue, billing and a new shell779 /// Usernames as given, tidied, if each names an account.
780 async fn valid_assignees(&self, names: Vec<String>) -> Result<Outcome<Vec<String>>> {
781 let mut assignees: Vec<String> = Vec::new();
782 for name in names {
783 let name = name.trim().trim_start_matches('@').to_lowercase();
784 if name.is_empty() || assignees.contains(&name) {
785 continue;
786 }
787 if assignees.len() == MAX_ASSIGNEES {
788 return Ok(Outcome::fail(
789 FailureCode::Invalid,
790 format!("An issue can be assigned to at most {MAX_ASSIGNEES} people."),
791 ));
792 }
793 let account: Viewer = g1t_kit::call(
794 &self.identity,
795 "user_by_username",
796 &UsernameArgs {
797 username: name.clone(),
798 },
799 )
800 .await?;
801 if account.is_none() {
802 return Ok(Outcome::fail(
803 FailureCode::Invalid,
804 format!("There is no account named {name}."),
805 ));
806 }
807 assignees.push(name);
808 }
809 Ok(Outcome::Ok(assignees))
810 }
811
812 /// Open issues assigned to the viewer, in every repository. Callers
813 /// show only those in repositories the viewer can still see.
814 async fn list_assigned_issues(&self, a: ViewerArgs) -> Result<Vec<Issue>> {
815 let Some(viewer) = a.viewer else {
816 return Ok(Vec::new());
817 };
818 let rows = self
819 .db
820 .prepare(format!(
821 "SELECT {ISSUE_COLUMNS} FROM issues
822 WHERE state = 'open' AND EXISTS (
823 SELECT 1 FROM json_each(issues.assignees) WHERE json_each.value = ?)
824 ORDER BY updated_at DESC LIMIT 50"
825 ))
826 .bind(&[viewer.username.into()])?
827 .all()
828 .await?
829 .results::<IssueRow>()?;
830 Ok(rows.into_iter().map(Issue::from).collect())
831 }
832
Pull requests from branches833 async fn close_issue(&self, a: IssueActionArgs) -> Result<Outcome<Issue>> {
834 let mut issue = check!(self.manageable_issue(&a.actor, &a.repo, a.number).await?);
835 if issue.state == State::Closed {
836 return Ok(Outcome::fail(
837 FailureCode::Conflict,
838 "This issue is already closed.",
839 ));
840 }
841 let reason = a.reason.unwrap_or(IssueReason::Completed);
842 let now = rfc3339(now_ms());
843 self.db
844 .prepare(
845 "UPDATE issues SET state = 'closed', reason = ?, closed_at = ?, updated_at = ?
846 WHERE id = ?",
847 )
848 .bind(&[
849 reason.as_str().into(),
850 now.as_str().into(),
851 now.as_str().into(),
852 issue.id.as_str().into(),
853 ])?
854 .run()
855 .await?;
856 self.publish(
857 "issue.closed",
858 &issue.repo_id,
859 &a.actor,
860 IssueEvent {
861 reason: Some(reason.as_str()),
862 ..Self::issue_event(&issue)
863 },
864 )
865 .await?;
Agents as a team: lifecycle, merge queue, billing and a new shell866 self.note(
867 &issue.repo_id,
868 issue.number,
869 (&a.actor.id, &a.actor.username),
870 match reason {
871 IssueReason::Completed => "closed this as completed",
872 IssueReason::NotPlanned => "closed this as not planned",
873 },
874 )
875 .await?;
Pull requests from branches876 issue.state = State::Closed;
877 issue.reason = Some(reason);
878 issue.closed_at = Some(now.clone());
879 issue.updated_at = now;
880 Ok(Outcome::Ok(issue))
881 }
882
883 async fn reopen_issue(&self, a: IssueActionArgs) -> Result<Outcome<Issue>> {
884 let mut issue = check!(self.manageable_issue(&a.actor, &a.repo, a.number).await?);
885 if issue.state == State::Open {
886 return Ok(Outcome::fail(
887 FailureCode::Conflict,
888 "This issue is already open.",
889 ));
890 }
891 let now = rfc3339(now_ms());
892 self.db
893 .prepare(
894 "UPDATE issues
895 SET state = 'open', reason = NULL, resolved_by = NULL, closed_at = NULL,
896 updated_at = ?
897 WHERE id = ?",
898 )
899 .bind(&[now.as_str().into(), issue.id.as_str().into()])?
900 .run()
901 .await?;
902 self.publish(
903 "issue.reopened",
904 &issue.repo_id,
905 &a.actor,
906 Self::issue_event(&issue),
907 )
908 .await?;
Agents as a team: lifecycle, merge queue, billing and a new shell909 self.note(
910 &issue.repo_id,
911 issue.number,
912 (&a.actor.id, &a.actor.username),
913 "reopened this",
914 )
915 .await?;
Pull requests from branches916 issue.state = State::Open;
917 issue.reason = None;
918 issue.resolved_by = None;
919 issue.closed_at = None;
920 issue.updated_at = now;
921 Ok(Outcome::Ok(issue))
922 }
923
924
925 async fn counts(&self, a: ViewArgs) -> Result<Outcome<Counts>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily926 let read = |repo_id: String| async move {
927 let query = self
928 .db
929 .prepare(
930 "SELECT
931 (SELECT count(*) FROM issues WHERE repo_id = ?1 AND state = 'open') AS issues,
932 (SELECT count(*) FROM pulls
933 WHERE repo_id = ?1 AND status IN ('draft', 'open')) AS pulls",
934 )
935 .bind(&[repo_id.into()])?;
936 self.timing.db(1, query.first::<Counts>(None)).await
937 };
938 let (_, counts) = check!(self.repo_then(&a.repo, &a.viewer, read).await?);
Pull requests from branches939 Ok(Outcome::Ok(counts.unwrap_or(Counts {
940 issues: 0,
941 pulls: 0,
942 })))
943 }
944
945 // --- Comments ----------------------------------------------------------
946
947 async fn add_comment(&self, a: AddCommentArgs) -> Result<Outcome<Comment>> {
948 if !a.actor.verified {
949 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
950 }
951 let body = a.body.trim();
Acceptance checks in sandboxes, line comments and review verdicts952 // An approval speaks for itself; anything else has to say something.
953 if body.is_empty() && a.verdict != Some(Verdict::Approve) {
Pull requests from branches954 return Ok(Outcome::fail(
955 FailureCode::Invalid,
956 "A comment cannot be empty.",
957 ));
958 }
Acceptance checks in sandboxes, line comments and review verdicts959 let path = a
960 .path
961 .as_deref()
962 .map(str::trim)
963 .filter(|path| !path.is_empty());
964 let line = a.line.filter(|line| *line > 0 && path.is_some());
Pull requests from branches965 if body.chars().count() > MAX_ENTRY_CHARS {
966 return Ok(Outcome::fail(
967 FailureCode::Invalid,
968 "That comment is too long.",
969 ));
970 }
971 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look972 check!(writable(&repo));
Pull requests from branches973 // The number names an issue or a pull request, never both.
Agents as a team: lifecycle, merge queue, billing and a new shell974 let mut pull_id = None;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar975 // A command to g1t on a dependency update it opened (`@g1t rebase`)
976 // is the security service's to act on, not a mention for an agent.
977 let mut update_command = false;
Pull requests from branches978 let table = if self.issue(&repo.id, a.number).await?.is_some() {
Acceptance checks in sandboxes, line comments and review verdicts979 if path.is_some() || a.verdict.is_some() {
980 return Ok(Outcome::fail(
981 FailureCode::Invalid,
982 "Only a pull request can be reviewed or commented on by line.",
983 ));
984 }
Pull requests from branches985 "issues"
Acceptance checks in sandboxes, line comments and review verdicts986 } else if let Some(pull) = self.pull(&repo.id, a.number).await? {
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights987 if a.verdict.is_some() && pull.is_owned_by(&a.actor.id) {
Acceptance checks in sandboxes, line comments and review verdicts988 return Ok(Outcome::fail(
989 FailureCode::Forbidden,
990 "You cannot approve or request changes on your own pull request.",
991 ));
992 }
Agents as a team: lifecycle, merge queue, billing and a new shell993 pull_id = Some(pull.id.clone());
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar994 update_command = pull.author.is_system()
995 && g1t_contracts::updates::update_command(body).is_some();
Pull requests from branches996 "pulls"
997 } else {
998 return Ok(Outcome::fail(
999 FailureCode::NotFound,
1000 "No issue or pull request has that number.",
1001 ));
1002 };
1003
1004 let now = now_ms();
1005 let comment = Comment {
Agents as a team: lifecycle, merge queue, billing and a new shell1006 kind: CommentKind::Comment,
Pull requests from branches1007 id: new_id("cmt", now),
1008 author: a.actor.clone(),
1009 body: body.to_owned(),
Acceptance checks in sandboxes, line comments and review verdicts1010 path: path.map(str::to_owned),
1011 line,
1012 verdict: a.verdict,
Pull requests from branches1013 created_at: rfc3339(now),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1014 edited_at: None,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1015 agent: None,
1016 acting_for: None,
1017 advisory: false,
Pull requests from branches1018 };
1019 self.db
1020 .batch(vec![
1021 self.db
1022 .prepare(
1023 "INSERT INTO comments
Acceptance checks in sandboxes, line comments and review verdicts1024 (id, repo_id, number, author_id, author_name, body, path, line,
1025 verdict, created_at)
1026 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Pull requests from branches1027 )
1028 .bind(&[
1029 comment.id.as_str().into(),
1030 repo.id.as_str().into(),
1031 a.number.into(),
1032 a.actor.id.as_str().into(),
1033 a.actor.username.as_str().into(),
1034 body.into(),
Acceptance checks in sandboxes, line comments and review verdicts1035 optional(&comment.path),
1036 optional_number(line),
1037 a.verdict
1038 .map_or(JsValue::NULL, |verdict| verdict.as_str().into()),
Pull requests from branches1039 comment.created_at.as_str().into(),
1040 ])?,
1041 self.db
1042 .prepare(format!(
1043 "UPDATE {table} SET updated_at = ? WHERE repo_id = ? AND number = ?"
1044 ))
1045 .bind(&[
1046 comment.created_at.as_str().into(),
1047 repo.id.as_str().into(),
1048 a.number.into(),
1049 ])?,
1050 ])
1051 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1052 if !update_command {
1053 self.note_mention(&a.actor, &repo, a.number, &comment, pull_id.as_deref()).await?;
1054 }
Pull requests from branches1055 self.publish(
1056 "comment.created",
1057 &repo.id,
1058 &a.actor,
1059 CommentCreated {
1060 comment_id: comment.id.clone(),
1061 repo_id: repo.id.clone(),
1062 number: a.number,
Agents as a team: lifecycle, merge queue, billing and a new shell1063 pull_id,
1064 verdict: a.verdict,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1065 ..CommentCreated::default()
Pull requests from branches1066 },
1067 )
1068 .await?;
1069 Ok(Outcome::Ok(comment))
1070 }
1071
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1072 /// A comment in the repository, with the repository and the pull
1073 /// request it is on (if it is on one), when `actor` may edit it, or
1074 /// with `deleting`, delete it (`may_change_comment`).
1075 async fn changeable_comment(
1076 &self,
1077 a: &CommentActionArgs,
1078 deleting: bool,
1079 ) -> Result<Outcome<(Repo, CommentRow, Option<String>)>> {
1080 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
1081 check!(writable(&repo));
1082 let Some(row) = self
1083 .db
1084 .prepare("SELECT * FROM comments WHERE id = ? AND repo_id = ?")
1085 .bind(&[a.comment_id.trim().into(), repo.id.as_str().into()])?
1086 .first::<CommentRow>(None)
1087 .await?
1088 else {
1089 return Ok(Outcome::fail(FailureCode::NotFound, "Comment not found."));
1090 };
1091 let maintains = access::can(Some(&a.actor), &repo, Capability::ManageSettings);
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1092 // What an agent wrote as itself is answered for by whoever it acted for.
1093 let author_id = row.answerable_id().to_owned();
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1094 if let Err(refusal) = may_change_comment(
1095 row.kind,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1096 row.has_verdict(),
1097 &author_id,
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1098 &a.actor.id,
1099 maintains,
1100 deleting,
1101 ) {
1102 // Someone who may change it otherwise was refused for what it is.
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1103 let code = if author_id == a.actor.id || maintains { FailureCode::Conflict } else { FailureCode::Forbidden };
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1104 return Ok(Outcome::fail(code, refusal));
1105 }
1106 let pull_id = self.pull(&repo.id, row.number).await?.map(|pull| pull.id);
1107 Ok(Outcome::Ok((repo, row, pull_id)))
1108 }
1109
1110 /// Changes the text of a comment: its author's to do, or a
1111 /// maintainer's. Publishes `comment.edited` with what it said before.
1112 async fn edit_comment(&self, a: CommentActionArgs) -> Result<Outcome<Comment>> {
1113 let body = a.body.trim().to_owned();
1114 if body.chars().count() > MAX_ENTRY_CHARS {
1115 return Ok(Outcome::fail(FailureCode::Invalid, "That comment is too long."));
1116 }
1117 let (repo, row, pull_id) = check!(self.changeable_comment(&a, false).await?);
1118 // An approval speaks for itself; anything else has to say something.
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar1119 if body.is_empty() && row.verdict != Some(Verdict::Approve) && row.agent_verdict.as_deref() != Some("approve") {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1120 return Ok(Outcome::fail(FailureCode::Invalid, "A comment cannot be empty."));
1121 }
1122 let number = row.number;
1123 let mut comment = Comment::from(row);
1124 if comment.body == body {
1125 return Ok(Outcome::Ok(comment));
1126 }
1127 let now = rfc3339(now_ms());
1128 self.db
1129 .prepare("UPDATE comments SET body = ?, edited_at = ? WHERE id = ?")
1130 .bind(&[body.as_str().into(), now.as_str().into(), comment.id.as_str().into()])?
1131 .run()
1132 .await?;
1133 let before = std::mem::replace(&mut comment.body, body);
1134 comment.edited_at = Some(now);
1135 self.publish(
1136 "comment.edited",
1137 &repo.id,
1138 &a.actor,
1139 CommentEdited {
1140 comment_id: comment.id.clone(),
1141 repo_id: repo.id.clone(),
1142 number,
1143 pull_id,
1144 changes: CommentChanges { body: ChangedFrom { from: before } },
1145 },
1146 )
1147 .await?;
1148 Ok(Outcome::Ok(comment))
1149 }
1150
1151 /// Deletes a comment: its author's to do, or a maintainer's. A review
1152 /// that gave a verdict stays. Publishes `comment.deleted` with the
1153 /// comment as it was.
1154 async fn delete_comment(&self, a: CommentActionArgs) -> Result<Outcome<bool>> {
1155 let (repo, row, pull_id) = check!(self.changeable_comment(&a, true).await?);
1156 self.db
1157 .prepare("DELETE FROM comments WHERE id = ?")
1158 .bind(&[row.id.as_str().into()])?
1159 .run()
1160 .await?;
1161 let number = row.number;
1162 let comment = Comment::from(row);
1163 self.publish(
1164 "comment.deleted",
1165 &repo.id,
1166 &a.actor,
1167 CommentDeleted {
1168 comment_id: comment.id.clone(),
1169 repo_id: repo.id.clone(),
1170 number,
1171 pull_id,
1172 comment: DeletedComment {
1173 id: comment.id,
1174 body: comment.body,
1175 author: (&comment.author).into(),
1176 created_at: comment.created_at,
1177 path: comment.path,
1178 line: comment.line,
1179 },
1180 },
1181 )
1182 .await?;
1183 Ok(Outcome::Ok(true))
1184 }
1185
Pull requests from branches1186 // --- Pull requests -----------------------------------------------------
1187
1188 async fn open_pull(&self, a: OpenPullArgs) -> Result<Outcome<Pull>> {
1189 if !a.actor.verified {
1190 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1191 }
1192 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1193 check!(writable(&repo));
1194 // g1t's own agent at work spends the workspace's compute; a pull
1195 // request anyone else's agent makes is like any other.
1196 if matches!(a.runtime, Runtime::Hosted) {
1197 check!(allowed(Some(&a.actor), &repo, Capability::Run));
1198 }
Pull requests from branches1199 let issue = match a.issue {
1200 Some(number) => match self.issue(&repo.id, number).await? {
1201 Some(issue) if issue.state == State::Open => Some(issue),
1202 Some(_) => {
1203 return Ok(Outcome::fail(
1204 FailureCode::Conflict,
1205 "This issue is closed.",
1206 ));
1207 }
1208 None => return Ok(no_issue()),
1209 },
1210 None => None,
1211 };
1212 // A pull request for an issue takes the issue's title unless given one.
1213 let title = match (a.title.trim(), &issue) {
1214 ("", Some(issue)) => issue.title.clone(),
1215 (title, _) => match valid_title(title) {
1216 Ok(title) => title.to_owned(),
1217 Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)),
1218 },
1219 };
Pull requests: unnamed, a pull request is its author's, not an agent's1220 // Unnamed, the change is its author's, unless an agent opened it.
Pull requests from branches1221 let agent = match a.agent.trim() {
Pull requests: unnamed, a pull request is its author's, not an agent's1222 "" if g1t_contracts::rules::is_agent(&a.actor) => "agent",
1223 "" => a.actor.username.as_str(),
Pull requests from branches1224 agent => agent,
1225 };
1226 let runtime = match a.runtime {
1227 Runtime::Hosted => "hosted",
1228 Runtime::External => "external",
1229 };
1230
1231 let now = now_ms();
1232 let id = new_id("pr", now);
1233 let branch = a
1234 .branch
1235 .as_deref()
1236 .map(str::trim)
1237 .filter(|branch| !branch.is_empty());
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1238 // The branch it merges into: the default branch unless another is
1239 // asked for, which has to exist.
1240 let base = a.base.as_deref().and_then(|base| stored_base(base, &repo));
1241 if let Some(base) = &base {
1242 if branch == Some(base.as_str()) {
1243 return Ok(Outcome::fail(
1244 FailureCode::Invalid,
1245 format!("A pull request cannot merge {base} into itself. Choose another base."),
1246 ));
1247 }
1248 let exists: Option<String> = g1t_kit::call(
1249 &self.repos,
1250 "head",
1251 &HeadArgs { repo_id: repo.id.clone(), branch: base.clone() },
1252 )
1253 .await?;
1254 if exists.is_none() {
1255 return Ok(Outcome::fail(FailureCode::NotFound, format!("There is no branch named {base} to merge into.")));
1256 }
1257 }
1258 let base_name = base.clone().unwrap_or_else(|| repo.default_branch.clone());
Pull requests from branches1259 // The change is on a branch already pushed to the repository, or
1260 // will be made in a fork created for this pull request.
1261 let (fork, head) = match branch {
1262 Some(branch) => {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1263 if branch == base_name {
Pull requests from branches1264 return Ok(Outcome::fail(
1265 FailureCode::Invalid,
1266 format!("Choose a branch other than {branch}."),
1267 ));
1268 }
1269 let head: Option<String> = g1t_kit::call(
1270 &self.repos,
1271 "head",
1272 &HeadArgs {
1273 repo_id: repo.id.clone(),
1274 branch: branch.to_owned(),
1275 },
1276 )
1277 .await?;
1278 let Some(head) = head else {
1279 return Ok(Outcome::fail(
1280 FailureCode::NotFound,
1281 format!("There is no branch named {branch}. Push it first."),
1282 ));
1283 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1284 // One open pull request for each branch and base.
Pull requests from branches1285 let existing = self
1286 .db
1287 .prepare(
1288 "SELECT number AS n FROM pulls
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1289 WHERE repo_id = ? AND source_branch = ? AND status IN ('draft', 'open')
1290 AND base_branch IS ?",
Pull requests from branches1291 )
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1292 .bind(&[repo.id.as_str().into(), branch.into(), optional(&base)])?
Pull requests from branches1293 .first::<NumberRow>(None)
1294 .await?;
1295 if let Some(existing) = existing {
1296 return Ok(Outcome::fail(
1297 FailureCode::Conflict,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1298 format!("Pull request #{} is already open from {branch} into {base_name}.", existing.n),
Pull requests from branches1299 ));
1300 }
1301 (None, Some(head))
1302 }
1303 None => {
1304 let fork: Outcome<Repo> = g1t_kit::call(
1305 &self.repos,
1306 "fork_for_pull",
1307 &ForkArgs {
1308 source_id: repo.id.clone(),
1309 pull_id: id.clone(),
1310 actor: a.actor.clone(),
1311 },
1312 )
1313 .await?;
1314 (Some(check!(fork)), None)
1315 }
1316 };
1317 // A branch already holds the work, so its pull request is ready for
1318 // review from the start; one with a fork starts as a draft.
Open a pull request as a draft: it can't merge, and agents' review routines wait, until it is marked ready1319 // Asked for as a draft, it waits until it is marked ready.
1320 let status = if branch.is_some() && !a.draft { "open" } else { "draft" };
Pull requests from branches1321 let body = Some(a.body.trim().to_owned()).filter(|body| !body.is_empty());
1322
1323 let number = self.next_number(&repo.id).await?;
1324 let timestamp = rfc3339(now);
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1325 // A change g1t makes is g1t's, for whoever asked for it. This is
1326 // what lifecycle::made_by_g1t reads back.
1327 let by_g1t = matches!(a.runtime, Runtime::Hosted) && agent == reviews::AGENT_NAME && fork.is_some();
1328 let (author, requested_by) = authorship(&a.actor, by_g1t);
Pull requests from branches1329 self.db
1330 .prepare(
1331 "INSERT INTO pulls
1332 (id, repo_id, number, issue_id, issue_number, title, body, agent, runtime,
1333 status, fork_repo_id, fork_namespace, fork_name, source_branch, head_commit,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1334 author_id, author_name, requested_by_id, requested_by_name, created_at, updated_at,
1335 base_branch)
1336 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Pull requests from branches1337 )
1338 .bind(&[
1339 id.as_str().into(),
1340 repo.id.as_str().into(),
1341 number.into(),
1342 optional(&issue.as_ref().map(|issue| issue.id.clone())),
1343 optional_number(issue.as_ref().map(|issue| issue.number)),
1344 title.into(),
1345 optional(&body),
1346 agent.into(),
1347 runtime.into(),
1348 status.into(),
1349 optional(&fork.as_ref().map(|fork| fork.id.clone())),
1350 optional(&fork.as_ref().map(|fork| fork.namespace.clone())),
1351 optional(&fork.as_ref().map(|fork| fork.name.clone())),
1352 optional(&branch.map(str::to_owned)),
1353 optional(&head),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1354 author.id.as_str().into(),
1355 author.username.as_str().into(),
1356 optional(&requested_by.as_ref().map(|user| user.id.clone())),
1357 optional(&requested_by.as_ref().map(|user| user.username.clone())),
Pull requests from branches1358 timestamp.as_str().into(),
1359 timestamp.as_str().into(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1360 optional(&base),
Pull requests from branches1361 ])?
1362 .run()
1363 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1364 let Some(mut pull) = self.pull(&repo.id, number).await? else {
Pull requests from branches1365 return Ok(no_pull());
1366 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1367 fill_base(&mut pull, &repo);
Agents as a team: lifecycle, merge queue, billing and a new shell1368 self.manage(&pull).await?;
1369 // Someone is on it now, so it is no longer waiting for an agent.
1370 if let Some(issue) = pull.issue {
1371 self.db
1372 .prepare("UPDATE issues SET queued_by = NULL WHERE repo_id = ? AND number = ?")
1373 .bind(&[repo.id.as_str().into(), issue.into()])?
1374 .run()
1375 .await?;
1376 }
1377 if let Some(issue) = pull.issue {
1378 let text = if lifecycle::made_by_g1t(&pull) {
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1379 format!("assigned this to g1t, which opened #{}", pull.number)
Agents as a team: lifecycle, merge queue, billing and a new shell1380 } else {
1381 format!("opened #{} for this", pull.number)
1382 };
1383 self.note(&repo.id, issue, (&a.actor.id, &a.actor.username), &text)
1384 .await?;
1385 }
Pull requests from branches1386 self.publish(
1387 "pull.opened",
1388 &repo.id,
1389 &a.actor,
1390 PullEvent {
1391 agent: Some(pull.agent.clone()),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1392 base: pull.base.clone(),
Pull requests from branches1393 ..Self::pull_event(&pull)
1394 },
1395 )
1396 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1397 // Its code owners asked to review (codeowners.rs).
1398 self.refresh_code_owners(&pull).await;
1399 let pull = self.pull(&repo.id, number).await?.unwrap_or(pull);
Pull requests from branches1400 Ok(Outcome::Ok(pull))
1401 }
1402
1403 async fn list_pulls(&self, a: ListPullsArgs) -> Result<Outcome<Vec<Pull>>> {
1404 let filter = match a.state {
1405 Some(State::Open) => "AND status IN ('draft', 'open')",
1406 Some(State::Closed) => "AND status IN ('merged', 'closed')",
1407 None => "",
1408 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1409 let label = a
1410 .label
1411 .map(|label| label.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase())
1412 .filter(|label| !label.is_empty());
1413 let milestone = a.milestone;
1414 let read = |repo_id: String| {
1415 let label = label.clone();
1416 async move {
1417 let query = self
1418 .db
1419 .prepare(format!(
1420 "SELECT {PULL_COLUMNS} FROM pulls WHERE repo_id = ? {filter}
1421 AND (? IS NULL OR EXISTS
1422 (SELECT 1 FROM json_each(pulls.labels) WHERE json_each.value = ?))
1423 AND (? IS NULL OR milestone = ?)
1424 ORDER BY number DESC LIMIT ?"
1425 ))
1426 .bind(&[
1427 repo_id.into(),
1428 optional(&label),
1429 optional(&label),
1430 optional_number(milestone),
1431 optional_number(milestone),
1432 LIST_PAGE.into(),
1433 ])?;
1434 self.timing.db(1, query.all()).await?.results::<PullRow>()
1435 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1436 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1437 let (repo, rows) = check!(self.repo_then(&a.repo, &a.viewer, read).await?);
1438 let base = a.base.map(|base| base.trim().to_owned()).filter(|base| !base.is_empty());
1439 Ok(Outcome::Ok(
1440 rows.into_iter()
1441 .map(|row| {
1442 let mut pull = Pull::from(row);
1443 fill_base(&mut pull, &repo);
1444 pull
1445 })
1446 .filter(|pull| base.as_deref().is_none_or(|base| pull.base.as_deref() == Some(base)))
1447 .collect(),
1448 ))
Pull requests from branches1449 }
1450
Fast pages, required checks on the branch, self-hosted runners, honest incidents1451 /// `pulls_for_repos`: what `list_pulls` gives, open and closed, for many
1452 /// repositories at once: one access check with repos for all of them
1453 /// and one query, instead of two of each per repository.
1454 async fn pulls_for_repos(&self, a: PullsForReposArgs) -> Result<Vec<RepoPulls>> {
1455 let ids: Vec<String> = a.repo_ids.into_iter().take(MAX_PULLS_FOR_REPOS).collect();
1456 if ids.is_empty() {
1457 return Ok(Vec::new());
1458 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1459 let limit = a.limit.clamp(1, LIST_PAGE);
1460 // The rows are read beside the access check, for every id asked
1461 // about; those of repositories the viewer cannot read are dropped.
1462 let asked = serde_json::to_string(&ids)?;
1463 let check = ReadableArgs { ids, viewer: a.viewer };
1464 let readable = self.timing.rpc(g1t_kit::call::<_, Vec<Repo>>(&self.repos, "readable", &check));
1465 let (readable, rows) = try_join(readable, self.timing.db(1, self.newest_pulls(asked, limit))).await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents1466 if readable.is_empty() {
1467 return Ok(Vec::new());
1468 }
1469 let mut answer: Vec<RepoPulls> = readable
1470 .iter()
1471 .map(|repo| RepoPulls { repo_id: repo.id.clone(), open: Vec::new(), closed: Vec::new() })
1472 .collect();
1473 for pull in rows.into_iter().map(Pull::from) {
1474 let Some(entry) = answer.iter_mut().find(|entry| entry.repo_id == pull.repo_id) else {
1475 continue;
1476 };
1477 match pull.status {
1478 PullStatus::Draft | PullStatus::Open => entry.open.push(pull),
1479 PullStatus::Merged | PullStatus::Closed => entry.closed.push(pull),
1480 }
1481 }
1482 for entry in &mut answer {
1483 entry.open.sort_by_key(|pull| std::cmp::Reverse(pull.number));
1484 entry.closed.sort_by_key(|pull| std::cmp::Reverse(pull.number));
1485 }
1486 Ok(answer)
1487 }
1488
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1489 /// The newest `limit` of each repository's open (draft or open) and
1490 /// closed (merged or closed) pull requests, for the ids in `ids` (JSON).
1491 async fn newest_pulls(&self, ids: String, limit: u32) -> Result<Vec<PullRow>> {
1492 self.db
1493 .prepare(format!(
1494 "SELECT * FROM (
1495 SELECT {PULL_COLUMNS}, ROW_NUMBER() OVER (
1496 PARTITION BY pulls.repo_id, pulls.status IN ('draft', 'open') ORDER BY pulls.number DESC
1497 ) AS place
1498 FROM pulls WHERE pulls.repo_id IN (SELECT value FROM json_each(?1))
1499 ) WHERE place <= ?2"
1500 ))
1501 .bind(&[ids.into(), limit.into()])?
1502 .all()
1503 .await?
1504 .results::<PullRow>()
1505 }
1506
Pull requests from branches1507 async fn get_pull(&self, a: ViewArgs) -> Result<Outcome<PullDetail>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1508 let number = a.number;
1509 // Every row the page and the lifecycle read, in one batch started
1510 // beside the access check; the helpers below read from it.
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1511 let namespace = a.repo.namespace.clone();
1512 let read = |repo_id: String| self.prefetch_pull(repo_id, namespace.clone(), number);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1513 let Outcome::Ok((repo, Some(found))) = self.repo_then(&a.repo, &a.viewer, read).await? else {
1514 return Ok(no_pull());
1515 };
1516 let Some(row) = found.first::<PullRow>(prefetch::Slot::Pull)? else {
1517 return Ok(no_pull());
Pull requests from branches1518 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1519 let stored = found.first::<StoredBehind>(prefetch::Slot::Pull)?;
1520 let issue = found.first::<IssueRow>(prefetch::Slot::Issue)?.map(Issue::from);
1521 let comments: Vec<Comment> =
1522 found.rows::<CommentRow>(prefetch::Slot::Comments)?.into_iter().map(Comment::from).collect();
1523 self.keep_prefetched(Some(found));
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1524 let default_branch = repo.default_branch.clone();
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1525 let detail = self.pull_detail(repo, Pull::from(row), issue, comments, stored, &a.viewer).await;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1526 self.keep_prefetched(None);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1527 // The branch it merges into, named, for whoever reads it.
1528 Ok(match detail? {
1529 Outcome::Ok(mut detail) => {
1530 if detail.pull.base.as_deref().is_none_or(str::is_empty) {
1531 detail.pull.base = Some(default_branch);
1532 }
1533 Outcome::Ok(detail)
1534 }
1535 failed => failed,
1536 })
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1537 }
1538
1539 async fn pull_detail(
1540 &self,
1541 repo: Repo,
1542 mut pull: Pull,
1543 issue: Option<Issue>,
1544 comments: Vec<Comment>,
1545 stored: Option<StoredBehind>,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1546 viewer: &Viewer,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1547 ) -> Result<Outcome<PullDetail>> {
1548 // Whether it is behind, as worked out with its mergeability on the
1549 // last push to either side (mergeability.rs), when that was for
1550 // its head as it is now; otherwise asked of the repos service.
1551 let known_behind = stored.and_then(|stored| stored.for_head(pull.head_commit.as_deref()));
Agents as a team: lifecycle, merge queue, billing and a new shell1552 // Worked out on each push; this covers a pull request from before
1553 // that was recorded.
1554 if pull.files.is_empty() && pull.head_commit.is_some() {
1555 pull.files = self.refresh_files(&pull).await?;
1556 }
1557 // Everything else at once: none of it depends on the rest, and each
1558 // is a round trip of its own.
1559 let standing = async {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1560 // Mergeability first: where g1t sees a pull request through, a
1561 // conflict decides its next step.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1562 let behind = async {
1563 match known_behind {
1564 Some(behind) => Ok(behind),
1565 None => {
1566 let behind = self.is_behind(&repo.id, &pull).await?;
1567 // Kept for the next view when the mergeability on
1568 // record is for this head: a pull request from
1569 // before `behind` was kept asks once.
1570 if let Some(head) = pull.head_commit.as_deref()
1571 && pull.status.is_active()
1572 {
1573 self.db
1574 .prepare(
1575 "UPDATE pulls SET behind = ?1
1576 WHERE id = ?2 AND behind IS NULL AND mergeable_key LIKE ?3 || '..%'",
1577 )
1578 .bind(&[u32::from(behind).into(), pull.id.as_str().into(), head.into()])?
1579 .run()
1580 .await?;
1581 }
1582 Ok(behind)
1583 }
1584 }
1585 };
1586 let (merge, behind) = try_join(self.mergeability(&pull), behind).await?;
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1587 let assessed = self.assess_with_confidence(&pull, &issue, behind).await?;
1588 let confidence = assessed.as_ref().and_then(|(_, _, confidence)| confidence.clone());
1589 let lifecycle = assessed.map(|(lifecycle, _, _)| lifecycle);
1590 Ok::<_, worker::Error>((behind, (lifecycle, confidence), merge))
Agents as a team: lifecycle, merge queue, billing and a new shell1591 };
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1592 let (((behind, (lifecycle, confidence), (mergeable, conflicts)), (landing, stalled), comments), (checks, overlaps, review_pending)) =
Agents as a team: lifecycle, merge queue, billing and a new shell1593 try_join(
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1594 try_join3(standing, self.landing_state(&pull.id), async { Ok(comments) }),
Agents as a team: lifecycle, merge queue, billing and a new shell1595 try_join3(
1596 self.latest_checks(&pull.id),
1597 self.overlaps(&pull),
1598 self.review_pending(&pull.id),
1599 ),
1600 )
1601 .await?;
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1602 // As just worked out, rather than as it was read.
1603 if confidence.is_some() {
1604 pull.confidence = confidence;
1605 }
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1606 // The rules of the branch it merges into, as they stack, and which
1607 // of them it does not meet yet, for whoever is looking.
1608 let (statuses, settings, gate) = try_join3(
1609 self.statuses(&repo.id, pull.head_commit.as_deref()),
1610 self.settings_on(&repo, &pull),
1611 async {
1612 if pull.status.is_active() {
1613 self.merge_gate(&repo, &pull, viewer.as_ref(), false, true).await.map(Some)
1614 } else {
1615 Ok(None)
1616 }
1617 },
1618 )
1619 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1620 let code_owners = self.pull_code_owners(&pull, &comments, &settings).await?;
Pull requests from branches1621 Ok(Outcome::Ok(PullDetail {
Fast pages, required checks on the branch, self-hosted runners, honest incidents1622 required_checks: required_checks(&settings.required_checks, &statuses),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1623 rules: gate.map(|gate| rulesets::merge_rules(&gate.judged, &gate.requirements, pull.base_branch(&repo.default_branch) == repo.default_branch)),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1624 code_owners,
Agents as a team: lifecycle, merge queue, billing and a new shell1625 comments,
1626 checks,
1627 overlaps,
1628 behind,
1629 review_pending,
1630 lifecycle,
1631 landing,
1632 stalled,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request1633 messages: self.messages(&pull.id).await?,
Fast pages, required checks on the branch, self-hosted runners, honest incidents1634 statuses,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1635 mergeable,
1636 conflicts,
1637 earlier_checks: self.earlier_checks(&pull.id).await?,
Pull requests from branches1638 issue,
1639 pull,
1640 }))
1641 }
1642
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1643 /// The pull request, whatever its status, if its repository is not
1644 /// archived and `actor` opened it or may triage its pull requests.
1645 async fn managed_pull(
Pull requests from branches1646 &self,
1647 actor: &User,
1648 path: &RepoPath,
1649 number: u32,
1650 ) -> Result<Outcome<Pull>> {
1651 let (repo, pull) = check!(self.pull_at(path, number, &Some(actor.clone())).await?);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1652 check!(writable(&repo));
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1653 if !pull.is_owned_by(&actor.id) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1654 check!(allowed(Some(actor), &repo, Capability::Triage));
Pull requests from branches1655 }
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1656 Ok(Outcome::Ok(pull))
1657 }
1658
1659 /// The pull request, if it is still active and `actor` opened it or
1660 /// may triage the repository's pull requests.
1661 async fn manageable_pull(
1662 &self,
1663 actor: &User,
1664 path: &RepoPath,
1665 number: u32,
1666 ) -> Result<Outcome<Pull>> {
1667 let pull = check!(self.managed_pull(actor, path, number).await?);
Pull requests from branches1668 if !pull.status.is_active() {
1669 return Ok(Outcome::fail(
1670 FailureCode::Conflict,
1671 format!("This pull request is already {}.", pull.status.as_str()),
1672 ));
1673 }
1674 Ok(Outcome::Ok(pull))
1675 }
1676
Catching up with main takes seconds when the two sides touched different files1677 /// Brings a pull request up to date with the default branch without a
1678 /// sandbox, where the repos service can do that safely. Whoever could
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1679 /// have pushed the merge themselves may ask: whoever opened it (or asked
1680 /// g1t for it), for a fork; anyone who may push, for a branch of the
1681 /// repository. When it needs a
Catching up with main takes seconds when the two sides touched different files1682 /// real merge, says so, naming the conflicting files if a probe found
1683 /// them, and pushes nothing.
1684 async fn catch_up_pull(&self, a: PullActionArgs) -> Result<Outcome<PullBranchUpdate>> {
1685 let (repo, pull) = check!(self.pull_at(&a.repo, a.number, &Some(a.actor.clone())).await?);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1686 check!(writable(&repo));
Catching up with main takes seconds when the two sides touched different files1687 if !pull.status.is_active() {
1688 return Ok(Outcome::fail(
1689 FailureCode::Conflict,
1690 format!("This pull request is already {}.", pull.status.as_str()),
1691 ));
1692 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1693 if pull.fork_repo_id.is_some() {
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1694 if !pull.is_owned_by(&a.actor.id) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1695 return Ok(Outcome::fail(
1696 FailureCode::Forbidden,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1697 "Only whoever opened this pull request, or asked g1t for it, can update it.",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1698 ));
1699 }
Catching up with main takes seconds when the two sides touched different files1700 } else {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1701 check!(allowed(Some(&a.actor), &repo, Capability::Push));
Catching up with main takes seconds when the two sides touched different files1702 }
1703 let updated: Outcome<PullBranchUpdate> = g1t_kit::call(
1704 &self.repos,
1705 "update_pull_branch",
1706 &UpdatePullBranchArgs {
1707 source_id: pull.fork_repo_id.clone().unwrap_or_else(|| repo.id.clone()),
1708 branch: pull.branch.clone(),
1709 number: pull.number,
1710 actor: a.actor,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1711 target_branch: pull.base.clone(),
Catching up with main takes seconds when the two sides touched different files1712 },
1713 )
1714 .await?;
1715 // A probe that found conflicts says more than "both changed it".
1716 if let Outcome::Ok(PullBranchUpdate::NeedsAgent { .. }) = &updated
1717 && let Some(files) = self.conflicting_files(&pull).await?
1718 && !files.is_empty()
1719 {
1720 return Ok(Outcome::Ok(PullBranchUpdate::NeedsAgent {
1721 reason: NeedsAgentReason::Conflicting,
1722 detail: "Merging it conflicts.".to_owned(),
1723 paths: files,
1724 }));
1725 }
1726 Ok(updated)
1727 }
1728
Agents as a team: lifecycle, merge queue, billing and a new shell1729 async fn update_pull(&self, a: UpdatePullArgs) -> Result<Outcome<Pull>> {
1730 let pull = check!(self.manageable_pull(&a.actor, &a.repo, a.number).await?);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1731 // The branch it merges into, its milestone and its labels first:
1732 // each can be refused, and then nothing else changes.
1733 if a.base.is_some() || a.milestone.is_some() || a.labels.is_some() {
1734 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
1735 if let Some(base) = &a.base {
1736 check!(self.change_base(&a.actor, &repo, &pull, base).await?);
1737 }
1738 if let Some(number) = a.milestone {
1739 check!(self.set_milestone(&a.actor, &repo, &labels::Item::Pull(pull.clone()), number).await?);
1740 }
1741 if let Some(labels) = &a.labels {
1742 check!(self.relabel(&a.actor, &repo, &labels::Item::Pull(pull.clone()), labels).await?);
1743 }
1744 }
Agents as a team: lifecycle, merge queue, billing and a new shell1745 let assignees = match a.assignees {
1746 Some(names) => Some(check!(self.valid_assignees(names).await?)),
1747 None => None,
1748 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1749 // Teams, named `workspace/team`, apart from the people.
1750 let (team_names, a_reviewers) = match a.reviewers {
1751 Some(names) => {
1752 let (teams, people): (Vec<String>, Vec<String>) =
1753 names.into_iter().partition(|name| team_reviews::team_name(name).is_some());
1754 (Some(teams), Some(people))
1755 }
1756 None => (None, None),
1757 };
1758 let teams = match team_names {
1759 Some(names) => Some(check!(self.valid_team_reviewers(&a.actor, &a.repo, &pull, names).await?)),
1760 None => None,
1761 };
1762 let reviewers = match a_reviewers {
Agents as a team: lifecycle, merge queue, billing and a new shell1763 Some(names) => {
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1764 // g1t is not an account; everyone else has to be.
Agents as a team: lifecycle, merge queue, billing and a new shell1765 let agent = names
1766 .iter()
1767 .any(|name| name.trim().eq_ignore_ascii_case(reviews::AGENT_NAME));
1768 let people = names
1769 .into_iter()
1770 .filter(|name| !name.trim().eq_ignore_ascii_case(reviews::AGENT_NAME))
1771 .collect();
1772 let mut reviewers = check!(self.valid_assignees(people).await?);
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1773 // Nobody is asked to review their own, nor what they had g1t make.
1774 reviewers.retain(|name| *name != pull.owner().username);
Agents as a team: lifecycle, merge queue, billing and a new shell1775 if agent {
1776 reviewers.insert(0, reviews::AGENT_NAME.to_owned());
1777 }
1778 Some(reviewers)
1779 }
1780 None => None,
1781 };
1782 self.db
1783 .prepare(
1784 "UPDATE pulls
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1785 SET assignees = COALESCE(?, assignees), updated_at = ?
Agents as a team: lifecycle, merge queue, billing and a new shell1786 WHERE id = ?",
1787 )
1788 .bind(&[
1789 optional(&assignees.as_ref().map(serde_json::to_string).transpose()?),
1790 rfc3339(now_ms()).into(),
1791 pull.id.as_str().into(),
1792 ])?
1793 .run()
1794 .await?;
1795 if let Some(assignees) = &assignees {
1796 self.note_changes(
1797 &pull.repo_id,
1798 pull.number,
1799 &a.actor,
1800 &pull.assignees,
1801 assignees,
1802 ("assigned", "unassigned"),
1803 )
1804 .await?;
1805 }
Events: review requests, assignments, stops and deployments are published1806 // Who was newly assigned or asked to review, and whose request was
1807 // withdrawn: the inbox tells them, and webhooks say so.
1808 let newly = |after: &[String], before: &[String]| -> Vec<String> {
1809 after.iter().filter(|name| !before.contains(name)).cloned().collect()
1810 };
1811 if let Some(assignees) = &assignees {
1812 let added = newly(assignees, &pull.assignees);
1813 if !added.is_empty() {
1814 self.publish(
1815 "pull.assigned",
1816 &pull.repo_id,
1817 &a.actor,
1818 PullEvent {
1819 assignees: Some(assignees.clone()),
1820 added: Some(added),
1821 ..Self::pull_event(&pull)
1822 },
1823 )
1824 .await?;
1825 }
1826 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1827 if reviewers.is_some() || teams.is_some() {
1828 let people = reviewers.unwrap_or_else(|| pull.reviewers.clone());
1829 let teams = teams.unwrap_or_else(|| pull.team_reviewers.clone());
1830 self.set_reviewers(&pull, people, teams, Some(&a.actor), false).await?;
Events: review requests, assignments, stops and deployments are published1831 }
Agents as a team: lifecycle, merge queue, billing and a new shell1832 Ok(match self.pull(&pull.repo_id, pull.number).await? {
1833 Some(pull) => Outcome::Ok(pull),
1834 None => no_pull(),
1835 })
1836 }
1837
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1838 /// Points an open pull request at another branch to merge into. Needs
1839 /// the Write role. What it would merge, whether it is behind, its
1840 /// mergeability and its checks are all worked out against the new base.
1841 async fn change_base(&self, actor: &User, repo: &Repo, pull: &Pull, base: &str) -> Result<Outcome<()>> {
1842 check!(allowed(Some(actor), repo, Capability::Push));
1843 let base = base.trim();
1844 if base.is_empty() {
1845 return Ok(Outcome::fail(FailureCode::Invalid, "Name the branch it should merge into."));
1846 }
1847 let before = pull.base_branch(&repo.default_branch).to_owned();
1848 if base == before {
1849 return Ok(Outcome::Ok(()));
1850 }
1851 if pull.fork_repo_id.is_none() && pull.branch.as_deref() == Some(base) {
1852 return Ok(Outcome::fail(
1853 FailureCode::Invalid,
1854 format!("A pull request cannot merge {base} into itself. Choose another base."),
1855 ));
1856 }
1857 let exists: Option<String> = g1t_kit::call(
1858 &self.repos,
1859 "head",
1860 &HeadArgs { repo_id: repo.id.clone(), branch: base.to_owned() },
1861 )
1862 .await?;
1863 if exists.is_none() {
1864 return Ok(Outcome::fail(FailureCode::NotFound, format!("There is no branch named {base} to merge into.")));
1865 }
1866 // In the merge queue it was headed for the default branch; it
1867 // leaves the queue for another base.
1868 let left_queue = self
1869 .leave(&pull.repo_id, pull, QueueState::Removed, Some("Its base branch changed."))
1870 .await?;
1871 let stored = stored_base(base, repo);
1872 self.db
1873 .prepare(
1874 "UPDATE pulls
1875 SET base_branch = ?, updated_at = ?, land_requested = NULL, land_requested_at = NULL, behind = NULL,
1876 mergeable = NULL, mergeable_key = NULL, conflicts = NULL
1877 WHERE id = ?",
1878 )
1879 .bind(&[optional(&stored), rfc3339(now_ms()).into(), pull.id.as_str().into()])?
1880 .run()
1881 .await?;
1882 self.note(
1883 &pull.repo_id,
1884 pull.number,
1885 (&actor.id, &actor.username),
1886 &format!("changed the base branch from `{before}` to `{base}`"),
1887 )
1888 .await?;
1889 self.publish(
1890 "pull.base_changed",
1891 &pull.repo_id,
1892 actor,
1893 PullEvent { base: Some(base.to_owned()), ..Self::pull_event(pull) },
1894 )
1895 .await?;
1896 if left_queue {
1897 self.publish_as(
1898 "queue.changed",
1899 &pull.repo_id,
1900 None,
1901 g1t_contracts::events::QueueChanged { repo_id: pull.repo_id.clone() },
1902 )
1903 .await?;
1904 }
1905 // Whether it merges cleanly into the new base.
1906 if let Some(moved) = self.pull_by_id(&pull.id).await?
1907 && let Err(error) = self.assess_mergeability(&moved).await
1908 {
1909 worker::console_warn!("mergeability of {}: {error}", pull.id);
1910 }
1911 Ok(Outcome::Ok(()))
1912 }
1913
Pull requests from branches1914 /// Marks a draft ready for review, or updates the description of one
1915 /// that already is.
1916 async fn ready_pull(&self, a: PullActionArgs) -> Result<Outcome<Pull>> {
1917 let mut pull = check!(self.manageable_pull(&a.actor, &a.repo, a.number).await?);
1918 let summary = Some(a.summary.trim().to_owned()).filter(|summary| !summary.is_empty());
1919 let now = rfc3339(now_ms());
1920 self.db
1921 .prepare(
1922 "UPDATE pulls SET status = 'open', body = COALESCE(?, body), updated_at = ?
1923 WHERE id = ?",
1924 )
1925 .bind(&[
1926 optional(&summary),
1927 now.as_str().into(),
1928 pull.id.as_str().into(),
1929 ])?
1930 .run()
1931 .await?;
1932 if pull.status == PullStatus::Draft {
Workflows run when an agent's pull request is marked ready1933 // The head as it is now: the push that came just before may not
1934 // have reached `head_commit` yet, and workflows run on it.
1935 let commit = self.live_head(&pull).await?.or_else(|| pull.head_commit.clone());
Pull requests from branches1936 self.publish(
1937 "pull.ready",
1938 &pull.repo_id,
1939 &a.actor,
Workflows run when an agent's pull request is marked ready1940 PullEvent {
1941 commit,
1942 ..Self::pull_event(&pull)
1943 },
Pull requests from branches1944 )
1945 .await?;
1946 }
Agents as a team: lifecycle, merge queue, billing and a new shell1947 if pull.status == PullStatus::Draft {
1948 self.note(
1949 &pull.repo_id,
1950 pull.number,
1951 (&a.actor.id, &a.actor.username),
1952 "marked this ready for review",
1953 )
1954 .await?;
1955 }
Pull requests from branches1956 pull.status = PullStatus::Open;
1957 pull.body = summary.or(pull.body);
1958 pull.updated_at = now;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1959 // A draft's code owners are asked once it is ready.
1960 self.refresh_code_owners(&pull).await;
1961 if let Some(fresh) = self.pull(&pull.repo_id, pull.number).await? {
1962 pull.reviewers = fresh.reviewers;
1963 pull.team_reviewers = fresh.team_reviewers;
1964 }
Pull requests from branches1965 Ok(Outcome::Ok(pull))
1966 }
1967
1968 async fn close_pull(&self, a: PullActionArgs) -> Result<Outcome<Pull>> {
1969 let mut pull = check!(self.manageable_pull(&a.actor, &a.repo, a.number).await?);
1970 let now = rfc3339(now_ms());
1971 self.db
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1972 // What it was closed as, so that reopening brings that back.
1973 .prepare("UPDATE pulls SET status = 'closed', closed_from = ?, updated_at = ? WHERE id = ?")
1974 .bind(&[pull.status.as_str().into(), now.as_str().into(), pull.id.as_str().into()])?
Pull requests from branches1975 .run()
1976 .await?;
1977 self.publish(
1978 "pull.closed",
1979 &pull.repo_id,
1980 &a.actor,
1981 Self::pull_event(&pull),
1982 )
1983 .await?;
Agents as a team: lifecycle, merge queue, billing and a new shell1984 self.note(
1985 &pull.repo_id,
1986 pull.number,
1987 (&a.actor.id, &a.actor.username),
1988 "closed this",
1989 )
1990 .await?;
1991 // A closed pull request leaves the merge queue.
1992 if self
1993 .leave(&pull.repo_id, &pull, QueueState::Removed, Some("It was closed."))
1994 .await?
1995 {
1996 self.publish_as(
1997 "queue.changed",
1998 &pull.repo_id,
1999 None,
2000 g1t_contracts::events::QueueChanged {
2001 repo_id: pull.repo_id.clone(),
2002 },
2003 )
2004 .await?;
2005 }
Pull requests from branches2006 pull.status = PullStatus::Closed;
2007 pull.updated_at = now;
2008 Ok(Outcome::Ok(pull))
2009 }
2010
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts2011 /// Opens a closed pull request again: as the draft it was, if it was
2012 /// closed as one, else ready for review. A merged one stays merged.
2013 async fn reopen_pull(&self, a: PullActionArgs) -> Result<Outcome<Pull>> {
2014 let mut pull = check!(self.managed_pull(&a.actor, &a.repo, a.number).await?);
2015 if let Some(refusal) = reopen_refusal(pull.status) {
2016 return Ok(Outcome::fail(FailureCode::Conflict, refusal));
2017 }
2018 // The head as it is now, which workflows run on. A branch of the
2019 // repository that was deleted since leaves nothing to reopen; a
2020 // fork removed after the close is made again (repos, `pull.reopened`).
2021 let head = self.live_head(&pull).await?;
2022 if head.is_none() && pull.fork_repo_id.is_none() {
2023 return Ok(Outcome::fail(
2024 FailureCode::Conflict,
2025 format!(
2026 "The branch {} no longer exists. Push it again to reopen this pull request.",
2027 pull.branch.as_deref().unwrap_or_default()
2028 ),
2029 ));
2030 }
2031 let closed_from: Option<ClosedFrom> = self
2032 .db
2033 .prepare("SELECT closed_from FROM pulls WHERE id = ?")
2034 .bind(&[pull.id.as_str().into()])?
2035 .first(None)
2036 .await?;
2037 let status = reopened_status(closed_from.and_then(|row| row.closed_from).as_deref());
2038 let now = rfc3339(now_ms());
2039 self.db
2040 .prepare("UPDATE pulls SET status = ?, closed_from = NULL, superseded_by = NULL, updated_at = ? WHERE id = ?")
2041 .bind(&[status.as_str().into(), now.as_str().into(), pull.id.as_str().into()])?
2042 .run()
2043 .await?;
2044 self.publish(
2045 "pull.reopened",
2046 &pull.repo_id,
2047 &a.actor,
2048 PullEvent {
2049 commit: head.or_else(|| pull.head_commit.clone()),
2050 ..Self::pull_event(&pull)
2051 },
2052 )
2053 .await?;
2054 self.note(
2055 &pull.repo_id,
2056 pull.number,
2057 (&a.actor.id, &a.actor.username),
2058 "reopened this",
2059 )
2060 .await?;
2061 pull.status = status;
2062 pull.superseded_by = None;
2063 pull.updated_at = now;
2064 // Whether it still merges cleanly, now that it is open again.
2065 if let Err(error) = self.assess_mergeability(&pull).await {
2066 worker::console_warn!("mergeability of {}: {error}", pull.id);
2067 }
2068 Ok(Outcome::Ok(pull))
2069 }
2070
2071 /// Turns a pull request that is ready for review back into a draft: it
2072 /// cannot be merged until it is marked ready again, and it leaves the
2073 /// merge queue and any merge that was waiting for it to catch up.
2074 async fn convert_pull_to_draft(&self, a: PullActionArgs) -> Result<Outcome<Pull>> {
2075 let mut pull = check!(self.managed_pull(&a.actor, &a.repo, a.number).await?);
2076 if let Some(refusal) = draft_refusal(pull.status) {
2077 return Ok(Outcome::fail(FailureCode::Conflict, refusal));
2078 }
2079 let now = rfc3339(now_ms());
2080 self.db
2081 .prepare(
2082 "UPDATE pulls SET status = 'draft', land_requested = NULL, land_requested_at = NULL, updated_at = ?
2083 WHERE id = ?",
2084 )
2085 .bind(&[now.as_str().into(), pull.id.as_str().into()])?
2086 .run()
2087 .await?;
2088 self.publish(
2089 "pull.converted_to_draft",
2090 &pull.repo_id,
2091 &a.actor,
2092 Self::pull_event(&pull),
2093 )
2094 .await?;
2095 self.note(
2096 &pull.repo_id,
2097 pull.number,
2098 (&a.actor.id, &a.actor.username),
2099 "marked this as a draft",
2100 )
2101 .await?;
2102 if self
2103 .leave(&pull.repo_id, &pull, QueueState::Removed, Some("It was marked as a draft."))
2104 .await?
2105 {
2106 self.publish_as(
2107 "queue.changed",
2108 &pull.repo_id,
2109 None,
2110 g1t_contracts::events::QueueChanged {
2111 repo_id: pull.repo_id.clone(),
2112 },
2113 )
2114 .await?;
2115 }
2116 pull.status = PullStatus::Draft;
2117 pull.updated_at = now;
2118 Ok(Outcome::Ok(pull))
2119 }
2120
Pull requests from branches2121 /// Lands the pull request on the repository's default branch. Unless
2122 /// told to keep it open, that resolves the issue it was for: the issue
2123 /// closes naming this pull request, and the others still in progress
2124 /// for it close as superseded.
2125 async fn merge_pull(&self, a: PullActionArgs) -> Result<Outcome<Pull>> {
2126 let viewer = Some(a.actor.clone());
Agents as a team: lifecycle, merge queue, billing and a new shell2127 let (repo, pull) = check!(self.pull_at(&a.repo, a.number, &viewer).await?);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2128 check!(writable(&repo));
Pull requests from branches2129 match pull.status {
2130 PullStatus::Open => {}
2131 PullStatus::Draft => {
2132 return Ok(Outcome::fail(
2133 FailureCode::Conflict,
2134 "This pull request is still a draft. Mark it ready for review first.",
2135 ));
2136 }
2137 status => {
2138 return Ok(Outcome::fail(
2139 FailureCode::Conflict,
2140 format!("This pull request is already {}.", status.as_str()),
2141 ));
2142 }
2143 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2144 let base = pull.base_branch(&repo.default_branch).to_owned();
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2145 // The rules of the branch it merges into, as they stack: the one
2146 // gate for the merge button, the API, MCP, auto-merge and the queue,
2147 // for a person's pull request and an agent's alike. A bypass counts
2148 // when the merger asks for it, or for g1t when a ruleset lists it.
2149 let gate = self.merge_gate(&repo, &pull, Some(&a.actor), a.ignore_checks, true).await?;
2150 let bypassable = gate.bypassable();
2151 let gate = if a.bypass_rules || a.actor.is_system() { gate } else { gate.without_bypass() };
2152 let settings = rulesets::overlay(self.settings(&repo.id).await?, &gate.requirements, base == repo.default_branch);
2153 if pull.check_status == Some(CheckStatus::Failed) && !(a.ignore_checks && settings.allow_ignoring_checks) {
2154 return Ok(Outcome::fail(
2155 FailureCode::Conflict,
2156 "It failed in the merge queue; push a fix to try again.",
2157 ));
2158 }
2159 if let Some(refusal) = gate.refusal() {
2160 if access::can(Some(&a.actor), &repo, Capability::Merge) {
2161 self.record_merge_evaluations(&repo, &pull, &gate).await;
Acceptance checks in sandboxes, line comments and review verdicts2162 }
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2163 let offer = if bypassable && !a.bypass_rules {
2164 " You may bypass these rules: merge again and ask to bypass them (bypass_rules)."
2165 } else {
2166 ""
2167 };
2168 return Ok(Outcome::fail(FailureCode::Conflict, format!("{refusal}{offer}")));
Agents as a team: lifecycle, merge queue, billing and a new shell2169 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2170 // Known ahead of time to conflict: neither a merge nor the queue
2171 // would get through, so say what has to be resolved now.
2172 if let Some(files) = self.conflicting_files(&pull).await? {
2173 let named = if files.is_empty() {
2174 String::new()
2175 } else {
2176 format!(" in {}", files.join(", "))
2177 };
2178 return Ok(Outcome::fail(
2179 FailureCode::Conflict,
2180 format!(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2181 "This branch has conflicts with {base}{named} that must be resolved first. Have g1t resolve them, or merge {base} into it, fix them and push."
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2182 ),
2183 ));
2184 }
Pull requests from branches2185
Agents as a team: lifecycle, merge queue, billing and a new shell2186 // A repository that merges through a queue: it joins the queue, and
2187 // lands once its state together with everything ahead has passed.
2188 if settings.merge_queue {
2189 if !a.actor.verified {
2190 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
2191 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2192 check!(allowed(Some(&a.actor), &repo, Capability::Merge));
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2193 self.record_merge_evaluations(&repo, &pull, &gate).await;
Agents as a team: lifecycle, merge queue, billing and a new shell2194 return self.enqueue(&repo, &pull, &a.actor, a.keep_issue_open).await;
2195 }
2196
2197 // The default branch has moved under it. Unless the repository
2198 // insists on that being dealt with first, bring it up to date and
2199 // land it when that is done.
2200 if self.is_behind(&repo.id, &pull).await? {
2201 if settings.require_up_to_date {
2202 return Ok(Outcome::fail(
2203 FailureCode::Conflict,
2204 format!(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2205 "{base} has moved since this pull request was made, and this repository requires pull requests to be up to date before they merge. Catch up with {base} first."
Agents as a team: lifecycle, merge queue, billing and a new shell2206 ),
2207 ));
2208 }
2209 if !a.actor.verified {
2210 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
2211 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2212 check!(allowed(Some(&a.actor), &repo, Capability::Merge));
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2213 self.record_merge_evaluations(&repo, &pull, &gate).await;
Agents as a team: lifecycle, merge queue, billing and a new shell2214 self.request_landing(&pull, &a.actor, a.keep_issue_open)
2215 .await?;
2216 return Ok(Outcome::Ok(pull));
2217 }
2218
Pull requests from branches2219 // Whether the actor may write to the repository is decided by repos.
2220 let landed: Outcome<Landed> = g1t_kit::call(
2221 &self.repos,
2222 "land",
2223 &LandArgs {
2224 // A pull request from a branch lands from the repository itself.
2225 source_id: pull.fork_repo_id.clone().unwrap_or_else(|| repo.id.clone()),
2226 branch: pull.branch.clone(),
2227 actor: a.actor.clone(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2228 target_branch: Some(base.clone()),
Pull requests from branches2229 },
2230 )
2231 .await?;
2232 let landed = check!(landed);
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2233 self.record_merge_evaluations(&repo, &pull, &gate).await;
Agents as a team: lifecycle, merge queue, billing and a new shell2234 Ok(Outcome::Ok(
2235 self.record_merge(&repo, pull, &a.actor, a.keep_issue_open, landed)
2236 .await?,
2237 ))
2238 }
Pull requests from branches2239
Agents as a team: lifecycle, merge queue, billing and a new shell2240 /// Records a pull request as merged once the default branch holds it:
2241 /// closes its issue, supersedes the others for it, and says so.
2242 pub(crate) async fn record_merge(
2243 &self,
2244 repo: &Repo,
2245 mut pull: Pull,
2246 actor: &User,
2247 keep_issue_open: bool,
2248 landed: Landed,
2249 ) -> Result<Pull> {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2250 // Only a merge into the default branch resolves the issue: into
2251 // another branch, the work has not landed yet.
2252 let keep_issue_open = keep_issue_open || !pull.targets_default(&repo.default_branch);
Agents as a team: lifecycle, merge queue, billing and a new shell2253 let issue = match pull.issue {
2254 Some(number) if !keep_issue_open => self
2255 .issue(&repo.id, number)
2256 .await?
2257 .filter(|issue| issue.state == State::Open),
2258 _ => None,
2259 };
Pull requests from branches2260 let now = rfc3339(now_ms());
2261 let mut statements = vec![
2262 self.db
2263 .prepare(
2264 "UPDATE pulls
2265 SET status = 'merged', head_commit = ?, merge_base = ?, merged_by = ?,
2266 merged_at = ?, updated_at = ?
2267 WHERE id = ?",
2268 )
2269 .bind(&[
2270 landed.commit.as_str().into(),
2271 optional(&landed.previous),
Agents as a team: lifecycle, merge queue, billing and a new shell2272 actor.username.as_str().into(),
Pull requests from branches2273 now.as_str().into(),
2274 now.as_str().into(),
2275 pull.id.as_str().into(),
2276 ])?,
2277 ];
2278 if let Some(issue) = &issue {
2279 statements.push(
2280 self.db
2281 .prepare(
2282 "UPDATE issues
2283 SET state = 'closed', reason = 'completed', resolved_by = ?,
2284 closed_at = ?, updated_at = ?
2285 WHERE id = ?",
2286 )
2287 .bind(&[
2288 pull.number.into(),
2289 now.as_str().into(),
2290 now.as_str().into(),
2291 issue.id.as_str().into(),
2292 ])?,
2293 );
2294 statements.push(
2295 self.db
2296 .prepare(
2297 "UPDATE pulls SET status = 'closed', superseded_by = ?, updated_at = ?
2298 WHERE issue_id = ? AND id != ? AND status IN ('draft', 'open')",
2299 )
2300 .bind(&[
2301 pull.number.into(),
2302 now.as_str().into(),
2303 issue.id.as_str().into(),
2304 pull.id.as_str().into(),
2305 ])?,
2306 );
2307 }
2308 self.db.batch(statements).await?;
2309
2310 self.publish(
2311 "pull.merged",
2312 &repo.id,
Agents as a team: lifecycle, merge queue, billing and a new shell2313 actor,
Pull requests from branches2314 PullEvent {
2315 commit: Some(landed.commit.clone()),
2316 ..Self::pull_event(&pull)
2317 },
2318 )
2319 .await?;
2320 if let Some(issue) = &issue {
2321 self.publish(
2322 "issue.closed",
2323 &repo.id,
Agents as a team: lifecycle, merge queue, billing and a new shell2324 actor,
Pull requests from branches2325 IssueEvent {
2326 reason: Some(IssueReason::Completed.as_str()),
2327 resolved_by: Some(pull.number),
2328 ..Self::issue_event(issue)
2329 },
2330 )
2331 .await?;
2332 }
2333
Agents as a team: lifecycle, merge queue, billing and a new shell2334 let who = (actor.id.as_str(), actor.username.as_str());
2335 self.note(&repo.id, pull.number, who, "merged this").await?;
2336 if let Some(issue) = &issue {
2337 self.note(
2338 &repo.id,
2339 issue.number,
2340 who,
2341 &format!("closed this by merging #{}", pull.number),
2342 )
2343 .await?;
2344 }
Pull requests from branches2345 pull.status = PullStatus::Merged;
Agents as a team: lifecycle, merge queue, billing and a new shell2346 pull.head_commit = Some(landed.commit.clone());
Pull requests from branches2347 pull.merge_base = landed.previous;
Agents as a team: lifecycle, merge queue, billing and a new shell2348 pull.merged_by = Some(actor.username.clone());
Pull requests from branches2349 pull.merged_at = Some(now.clone());
2350 pull.updated_at = now;
Agents as a team: lifecycle, merge queue, billing and a new shell2351 Ok(pull)
Pull requests from branches2352 }
2353
2354 async fn list_active_pulls(&self, a: ViewerArgs) -> Result<Vec<ActivePull>> {
2355 let Some(viewer) = a.viewer else {
2356 return Ok(Vec::new());
2357 };
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2358 // The pull requests and their issues, in one round trip: their own,
2359 // and those g1t made for them (Pull::owner).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2360 let author = [JsValue::from(viewer.id.as_str())];
Agents as a team: lifecycle, merge queue, billing and a new shell2361 let found = self
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2362 .timing
2363 .db(
2364 2,
2365 self.db.batch(vec![
2366 self.db
2367 .prepare(format!(
2368 "SELECT {PULL_COLUMNS} FROM pulls
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2369 WHERE COALESCE(requested_by_id, author_id) = ?1 AND status IN ('draft', 'open')
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2370 ORDER BY updated_at DESC LIMIT 50"
2371 ))
2372 .bind(&author)?,
2373 self.db
2374 .prepare(format!(
2375 "SELECT {ISSUE_COLUMNS} FROM issues WHERE issues.id IN (
2376 SELECT issue_id FROM pulls
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2377 WHERE COALESCE(requested_by_id, author_id) = ?1 AND status IN ('draft', 'open') AND issue_id IS NOT NULL
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2378 ORDER BY updated_at DESC LIMIT 50)"
2379 ))
2380 .bind(&author)?,
2381 ]),
2382 )
Agents as a team: lifecycle, merge queue, billing and a new shell2383 .await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2384 let (Some(found), Some(issues)) = (found.first(), found.get(1)) else {
2385 return Ok(Vec::new());
2386 };
Agents as a team: lifecycle, merge queue, billing and a new shell2387 let snapshots = found.results::<Snapshot>()?;
2388 let pulls: Vec<Pull> = found.results::<PullRow>()?.into_iter().map(Pull::from).collect();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2389 let issues: Vec<Issue> = issues.results::<IssueRow>()?.into_iter().map(Issue::from).collect();
2390 let issues = &issues;
2391 // Where each stands: the remembered assessment when there is one,
2392 // and worked out otherwise.
Agents as a team: lifecycle, merge queue, billing and a new shell2393 try_join_all(pulls.into_iter().zip(snapshots).map(|(pull, snapshot)| async move {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2394 let issue = pull.issue.and_then(|number| {
2395 issues
2396 .iter()
2397 .find(|issue| issue.repo_id == pull.repo_id && issue.number == number)
2398 .cloned()
2399 });
Agents as a team: lifecycle, merge queue, billing and a new shell2400 // Only a pull request g1t is seeing through has a lifecycle.
2401 let lifecycle = if !lifecycle::made_by_g1t(&pull) || snapshot.managed == 0 {
2402 None
2403 } else if let (Some(stage), Some(detail)) = (snapshot.stage, snapshot.stage_detail) {
2404 Some(Lifecycle {
2405 stage,
2406 detail,
2407 revisions: snapshot.revisions,
2408 })
2409 } else {
2410 let behind = self.is_behind(&pull.repo_id, &pull).await?;
2411 self.assess(&pull, &issue, behind)
2412 .await?
2413 .map(|(lifecycle, _)| lifecycle)
2414 };
2415 Ok::<_, worker::Error>(ActivePull {
2416 pull,
2417 issue,
2418 lifecycle,
2419 })
2420 }))
2421 .await
Pull requests from branches2422 }
2423
2424 // --- Sessions ----------------------------------------------------------
2425
2426 async fn append_session(&self, a: AppendSessionArgs) -> Result<Outcome<Appended>> {
2427 if a.entries.is_empty() {
2428 return Ok(Outcome::Ok(Appended { count: 0 }));
2429 }
2430 if a.entries.len() > MAX_ENTRY_BATCH {
2431 return Ok(Outcome::fail(
2432 FailureCode::Invalid,
2433 format!("Send at most {MAX_ENTRY_BATCH} entries at a time."),
2434 ));
2435 }
2436 let viewer = Some(a.actor.clone());
2437 let (_, pull) = check!(self.pull_at(&a.repo, a.number, &viewer).await?);
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2438 if !pull.is_owned_by(&a.actor.id) {
Pull requests from branches2439 return Ok(Outcome::fail(
2440 FailureCode::Forbidden,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2441 "Only whoever opened a pull request, or asked g1t for it, can record its session.",
Pull requests from branches2442 ));
2443 }
2444
2445 let now = rfc3339(now_ms());
2446 let count = a.entries.len() as u32;
2447 let mut statements = Vec::with_capacity(a.entries.len() + 1);
2448 for entry in a.entries {
2449 let kind = serde_json::to_value(entry.kind)?;
2450 let text: String = entry.text.chars().take(MAX_ENTRY_CHARS).collect();
2451 // Each insert takes the next sequence number itself, so two
2452 // writers appending at once cannot collide.
2453 statements.push(
2454 self.db
2455 .prepare(
2456 "INSERT INTO session_entries (pull_id, seq, kind, text, tool, \"commit\", at)
2457 SELECT ?, COALESCE(MAX(seq), 0) + 1, ?, ?, ?, ?, ?
2458 FROM session_entries WHERE pull_id = ?",
2459 )
2460 .bind(&[
2461 pull.id.as_str().into(),
2462 kind.as_str().unwrap_or("note").into(),
2463 text.into(),
2464 optional(&entry.tool),
2465 optional(&entry.commit.or_else(|| pull.head_commit.clone())),
2466 now.as_str().into(),
2467 pull.id.as_str().into(),
2468 ])?,
2469 );
2470 }
2471 statements.push(
2472 self.db
2473 .prepare("UPDATE pulls SET updated_at = ? WHERE id = ?")
2474 .bind(&[now.as_str().into(), pull.id.as_str().into()])?,
2475 );
2476 self.db.batch(statements).await?;
2477 self.publish(
2478 "session.appended",
2479 &pull.repo_id,
2480 &a.actor,
2481 SessionAppended {
2482 pull_id: pull.id.clone(),
2483 repo_id: pull.repo_id.clone(),
2484 number: pull.number,
2485 count,
2486 },
2487 )
2488 .await?;
2489 Ok(Outcome::Ok(Appended { count }))
2490 }
2491
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request2492 /// Adds entries to a pull request's session, each taking the next
2493 /// sequence number, without announcing it.
2494 pub(crate) async fn append_entries(&self, pull: &Pull, entries: &[NewSessionEntry]) -> Result<()> {
2495 let now = rfc3339(now_ms());
2496 let mut statements = Vec::with_capacity(entries.len());
2497 for entry in entries {
2498 let kind = serde_json::to_value(entry.kind)?;
2499 let text: String = entry.text.chars().take(MAX_ENTRY_CHARS).collect();
2500 statements.push(
2501 self.db
2502 .prepare(
2503 "INSERT INTO session_entries (pull_id, seq, kind, text, tool, \"commit\", at)
2504 SELECT ?, COALESCE(MAX(seq), 0) + 1, ?, ?, ?, ?, ?
2505 FROM session_entries WHERE pull_id = ?",
2506 )
2507 .bind(&[
2508 pull.id.as_str().into(),
2509 kind.as_str().unwrap_or("note").into(),
2510 text.into(),
2511 optional(&entry.tool),
2512 optional(&entry.commit.clone().or_else(|| pull.head_commit.clone())),
2513 now.as_str().into(),
2514 pull.id.as_str().into(),
2515 ])?,
2516 );
2517 }
2518 self.db.batch(statements).await?;
2519 Ok(())
2520 }
2521
Pull requests from branches2522 async fn read_session(&self, a: ViewArgs) -> Result<Outcome<Vec<SessionEntry>>> {
2523 let (_, pull) = check!(self.pull_at(&a.repo, a.number, &a.viewer).await?);
2524 let rows = self
2525 .db
2526 .prepare(
2527 "SELECT seq, kind, text, tool, \"commit\", at FROM session_entries
2528 WHERE pull_id = ? AND seq > ? ORDER BY seq LIMIT ?",
2529 )
2530 .bind(&[pull.id.into(), a.after_seq.into(), SESSION_PAGE.into()])?
2531 .all()
2532 .await?
2533 .results::<SessionRow>()?;
2534 Ok(Outcome::Ok(
2535 rows.into_iter().map(SessionEntry::from).collect(),
2536 ))
2537 }
2538
Events service in Rust, with RFC 3339 times and accurate push events2539 /// A push moves the head of the pull request it concerns: the one whose
2540 /// fork was pushed to, or the one opened from the branch that moved.
2541 async fn on_event(&self, event: &Event) -> Result<()> {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2542 // A new repository starts with the default labels.
2543 if event.kind == "repo.created"
2544 && let Some(repo_id) = event.repo_id.as_deref()
2545 {
2546 return self.seed_labels(repo_id).await;
2547 }
2548 // Pull requests into the branch that became the default merge into
2549 // the default branch, which is stored as none.
2550 if event.kind == "repo.default_branch_changed"
2551 && let (Some(repo_id), Some(to)) = (event.repo_id.as_deref(), event.data["to"].as_str())
2552 {
2553 self.db
2554 .prepare(
2555 "UPDATE pulls SET base_branch = NULL
2556 WHERE repo_id = ? AND base_branch = ? AND status IN ('draft', 'open')",
2557 )
2558 .bind(&[repo_id.into(), to.into()])?
2559 .run()
2560 .await?;
2561 return Ok(());
2562 }
Pull requests from branches2563 if event.kind != "git.push" {
2564 return Ok(());
2565 }
Events service in Rust, with RFC 3339 times and accurate push events2566 let (Some(repo_id), Some(after), Some(git_ref)) = (
2567 event.repo_id.as_deref(),
2568 event.data["after"].as_str(),
2569 event.data["ref"].as_str(),
2570 ) else {
Pull requests from branches2571 return Ok(());
2572 };
2573 let now = rfc3339(now_ms());
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2574 // Who moved it, for rules about the most recent push.
2575 let pusher: JsValue = event.actor.as_deref().map_or(JsValue::NULL, Into::into);
Agents as a team: lifecycle, merge queue, billing and a new shell2576 // The head moved, so whatever the checks said no longer applies, and
2577 // whatever step g1t was waiting on has been taken.
Acceptance checks in sandboxes, line comments and review verdicts2578 let moved = "UPDATE pulls
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2579 SET head_commit = ?, updated_at = ?, head_pushed_by = ?, head_pushed_at = ?, check_status = NULL, check_run_id = NULL,
Agents as a team: lifecycle, merge queue, billing and a new shell2580 working_on = NULL, working_until = NULL, stalled = NULL";
Acceptance checks in sandboxes, line comments and review verdicts2581 let active = "status IN ('draft', 'open') AND head_commit IS NOT ?";
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2582 let returning =
2583 "RETURNING id, repo_id, number, issue_number, status, author_id, author_name, requested_by_id, requested_by_name";
Acceptance checks in sandboxes, line comments and review verdicts2584 let mut pulls: Vec<MovedRow> = Vec::new();
Events service in Rust, with RFC 3339 times and accurate push events2585 // A fork carries its pull request on its default branch.
2586 if event.data["defaultBranch"].as_bool() == Some(true) {
Acceptance checks in sandboxes, line comments and review verdicts2587 pulls.extend(
Events service in Rust, with RFC 3339 times and accurate push events2588 self.db
2589 .prepare(format!(
Acceptance checks in sandboxes, line comments and review verdicts2590 "{moved} WHERE fork_repo_id = ? AND {active} {returning}"
Events service in Rust, with RFC 3339 times and accurate push events2591 ))
Acceptance checks in sandboxes, line comments and review verdicts2592 .bind(&[
2593 after.into(),
2594 now.as_str().into(),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2595 pusher.clone(),
2596 now.as_str().into(),
Acceptance checks in sandboxes, line comments and review verdicts2597 repo_id.into(),
2598 after.into(),
2599 ])?
2600 .all()
2601 .await?
2602 .results::<MovedRow>()?,
Events service in Rust, with RFC 3339 times and accurate push events2603 );
2604 }
2605 if let Some(branch) = git_ref.strip_prefix("refs/heads/") {
Acceptance checks in sandboxes, line comments and review verdicts2606 pulls.extend(
Pull requests from branches2607 self.db
Events service in Rust, with RFC 3339 times and accurate push events2608 .prepare(format!(
Acceptance checks in sandboxes, line comments and review verdicts2609 "{moved} WHERE repo_id = ? AND source_branch = ? AND {active} {returning}"
Events service in Rust, with RFC 3339 times and accurate push events2610 ))
2611 .bind(&[
2612 after.into(),
2613 now.as_str().into(),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2614 pusher.clone(),
2615 now.as_str().into(),
Events service in Rust, with RFC 3339 times and accurate push events2616 repo_id.into(),
2617 branch.into(),
Acceptance checks in sandboxes, line comments and review verdicts2618 after.into(),
2619 ])?
2620 .all()
2621 .await?
2622 .results::<MovedRow>()?,
Events service in Rust, with RFC 3339 times and accurate push events2623 );
Pull requests from branches2624 }
Agents as a team: lifecycle, merge queue, billing and a new shell2625 // What each now changes, so overlaps show while the work is under way.
2626 for moved in &pulls {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2627 if let Some(mut pull) = self.pull_by_id(&moved.id).await? {
2628 pull.files = self.refresh_files(&pull).await?;
2629 // Owners of files it now changes are asked too.
2630 self.refresh_code_owners(&pull).await;
Agents as a team: lifecycle, merge queue, billing and a new shell2631 }
2632 }
2633 // A merge that was waiting for this push to bring it up to date.
2634 for moved in &pulls {
2635 self.land_if_requested(&moved.id).await?;
2636 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2637 // Whether each still merges cleanly, and, when a default branch
2638 // moved, every open pull request into it.
2639 let moved_ids: Vec<String> = pulls.iter().map(|pull| pull.id.clone()).collect();
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2640 let into = match event.data["defaultBranch"].as_bool() {
2641 Some(true) => mergeability::Moved::DefaultBranch,
2642 _ => match git_ref.strip_prefix("refs/heads/") {
2643 Some(branch) => mergeability::Moved::Branch(branch),
2644 None => mergeability::Moved::Nothing,
2645 },
2646 };
2647 self.after_push(repo_id, into, &moved_ids).await;
Acceptance checks in sandboxes, line comments and review verdicts2648 // A draft is announced when it is marked ready instead.
2649 for pull in pulls
2650 .into_iter()
2651 .filter(|pull| pull.status == PullStatus::Open)
2652 {
2653 self.publish_as(
2654 "pull.updated",
2655 &pull.repo_id,
2656 event.actor.clone(),
2657 PullEvent {
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2658 author: Some(g1t_contracts::credentials::Principal { id: pull.author_id, username: pull.author_name }),
2659 requested_by: pull
2660 .requested_by_id
2661 .zip(pull.requested_by_name)
2662 .map(|(id, username)| g1t_contracts::credentials::Principal { id, username }),
Acceptance checks in sandboxes, line comments and review verdicts2663 pull_id: pull.id,
2664 repo_id: pull.repo_id.clone(),
2665 number: pull.number,
2666 issue: pull.issue_number,
2667 commit: Some(after.to_owned()),
2668 ..PullEvent::default()
Merge branch 'worktree-agent-a3abfcce648e87dca'2669 }
2670 .carrying(&event.data),
Acceptance checks in sandboxes, line comments and review verdicts2671 )
2672 .await?;
2673 }
Pull requests from branches2674 Ok(())
2675 }
2676}
2677
Merge branch 'worktree-agent-a3abfcce648e87dca'2678/// An event's data that carries on what caused the event it follows from.
2679trait Carrying: serde::Serialize + Sized {
2680 /// As JSON, marked as a workflow job's doing when `cause` was.
2681 fn carrying(self, cause: &serde_json::Value) -> serde_json::Value {
2682 g1t_contracts::events::carried(self, cause)
2683 }
2684}
2685
2686impl Carrying for PullEvent {}
2687
Pull requests from branches2688fn service(env: &Env) -> Result<Work> {
2689 Ok(Work {
2690 db: env.d1("DB")?,
Agents as a team: lifecycle, merge queue, billing and a new shell2691 identity: env.service("IDENTITY")?,
Pull requests from branches2692 repos: env.service("REPOS")?,
Events service in Rust, with RFC 3339 times and accurate push events2693 events: env.service("EVENTS")?,
Sidebar: the panels really slide2694 actions: env.service("ACTIONS")?,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2695 timing: g1t_kit::d1::Timing::default(),
2696 prefetched: std::cell::RefCell::new(None),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2697 known_repos: std::cell::RefCell::new(std::collections::HashMap::new()),
Pull requests from branches2698 })
2699}
2700
2701#[event(fetch)]
2702async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
2703 let Some(method) = rpc_method(&request) else {
2704 return Response::error("Not found", 404);
2705 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents2706 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
2707 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
Pull requests from branches2708 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents2709 let mut work = service(&env)?;
2710 work.db = db;
Pull requests from branches2711
Fast pages, required checks on the branch, self-hosted runners, honest incidents2712 let answered = match method.as_str() {
Pull requests from branches2713 "open_issue" => reply(&work.open_issue(args(body)?).await?),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step2714 "delegate_issue" => reply(&work.delegate_issue(args(body)?).await?),
2715 "report_confidence" => reply(&work.report_confidence(args(body)?).await?),
Pull requests from branches2716 "list_issues" => reply(&work.list_issues(args(body)?).await?),
2717 "get_issue" => reply(&work.get_issue(args(body)?).await?),
2718 "update_issue" => reply(&work.update_issue(args(body)?).await?),
2719 "close_issue" => reply(&work.close_issue(args(body)?).await?),
2720 "reopen_issue" => reply(&work.reopen_issue(args(body)?).await?),
2721 "list_labels" => reply(&work.list_labels(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2722 "save_label" => reply(&work.save_label(args(body)?).await?),
2723 "delete_label" => reply(&work.delete_label(args(body)?).await?),
2724 "add_default_labels" => reply(&work.add_default_labels(args(body)?).await?),
2725 "set_labels" => reply(&work.set_labels(args(body)?).await?),
2726 "list_milestones" => reply(&work.list_milestones(args(body)?).await?),
2727 "get_milestone" => reply(&work.get_milestone(args(body)?).await?),
2728 "save_milestone" => reply(&work.save_milestone(args(body)?).await?),
2729 "delete_milestone" => reply(&work.delete_milestone(args(body)?).await?),
Pull requests from branches2730 "counts" => reply(&work.counts(args(body)?).await?),
2731 "add_comment" => reply(&work.add_comment(args(body)?).await?),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts2732 "edit_comment" => reply(&work.edit_comment(args(body)?).await?),
2733 "delete_comment" => reply(&work.delete_comment(args(body)?).await?),
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar2734 // A workspace's own agents, as themselves (agent_comments.rs). For
2735 // the agents service; never reachable with a person's token.
2736 "workspace_agent_comment" => reply(&work.workspace_agent_comment(args(body)?).await?),
2737 "workspace_agent_review" => reply(&work.workspace_agent_review(args(body)?).await?),
Acceptance checks in sandboxes, line comments and review verdicts2738 "start_checks" => reply(&work.start_checks(args(body)?).await?),
Fast pages, required checks on the branch, self-hosted runners, honest incidents2739 "seen_checks" => reply(&work.seen_checks(args(body)?).await?),
Acceptance checks in sandboxes, line comments and review verdicts2740 "report_checks" => reply(&work.report_checks(args(body)?).await?),
GitHub Actions on g1t, part two: running workflows2741 "set_commit_status" => reply(&work.set_commit_status(args(body)?).await?),
Merge checks: statuses and check runs on every commit2742 "create_commit_status" => reply(&work.create_commit_status(args(body)?).await?),
2743 "commit_statuses" => reply(&work.commit_statuses(args(body)?).await?),
2744 "combined_status" => reply(&work.combined_status(args(body)?).await?),
2745 "create_check_run" => reply(&work.create_check_run(args(body)?).await?),
2746 "update_check_run" => reply(&work.update_check_run(args(body)?).await?),
2747 "get_check_run" => reply(&work.get_check_run(args(body)?).await?),
2748 "check_run_annotations" => reply(&work.check_run_annotations(args(body)?).await?),
2749 "ref_check_runs" => reply(&work.ref_check_runs(args(body)?).await?),
2750 "ref_check_suites" => reply(&work.ref_check_suites(args(body)?).await?),
2751 "get_check_suite" => reply(&work.get_check_suite(args(body)?).await?),
2752 "rerequest_check_run" => reply(&work.rerequest_check_run(args(body)?).await?),
2753 "rerequest_check_suite" => reply(&work.rerequest_check_suite(args(body)?).await?),
2754 "request_check_action" => reply(&work.request_check_action(args(body)?).await?),
2755 "commit_checks" => reply(&work.commit_checks(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2756 "start_review" => reply(&work.start_review(args(body)?).await?),
2757 "advance" => reply(&work.advance(args(body)?).await?),
2758 "stall" => reply(&work.stall(args(body)?).await?),
2759 "managed_pulls" => reply(&work.managed_pulls(args(body)?).await?),
2760 "queue" => reply(&work.queue(args(body)?).await?),
2761 "queue_build" => reply(&work.queue_build(args(body)?).await?),
2762 "report_queue" => reply(&work.report_queue(args(body)?).await?),
2763 "remove_from_queue" => reply(&work.remove_from_queue(args(body)?).await?),
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request2764 "message_agent" => reply(&work.message_agent(args(body)?).await?),
Record your own agent's sessions automatically2765 "locate_pull" => reply(&work.locate_pull(args(body)?).await?),
Inbox: the events service tells people what needs them as events arrive2766 "inbox_subject" => reply(&work.inbox_subject(args(body)?).await?),
Agents ask each other, hand each other work, and answer2767 "answer_message" => reply(&work.answer_message(args(body)?).await?),
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request2768 "take_messages" => reply(&work.take_messages(args(body)?).await?),
Agents asked while not at work are woken to answer2769 "wake_for_messages" => reply(&work.wake_for_messages(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2770 "catch_up_job" => reply(&work.catch_up_job(args(body)?).await?),
2771 "get_settings" => reply(&work.get_settings(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2772 "codeowners_errors" => reply(&work.codeowners_errors(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2773 "update_settings" => reply(&work.update_settings(args(body)?).await?),
2774 "report_review" => reply(&work.report_review(args(body)?).await?),
Pull requests from branches2775 "open_pull" => reply(&work.open_pull(args(body)?).await?),
2776 "list_pulls" => reply(&work.list_pulls(args(body)?).await?),
Fast pages, required checks on the branch, self-hosted runners, honest incidents2777 "pulls_for_repos" => reply(&work.pulls_for_repos(args(body)?).await?),
Pull requests from branches2778 "get_pull" => reply(&work.get_pull(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2779 "update_pull" => reply(&work.update_pull(args(body)?).await?),
Catching up with main takes seconds when the two sides touched different files2780 "catch_up_pull" => reply(&work.catch_up_pull(args(body)?).await?),
Pull requests from branches2781 "ready_pull" => reply(&work.ready_pull(args(body)?).await?),
2782 "close_pull" => reply(&work.close_pull(args(body)?).await?),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts2783 "reopen_pull" => reply(&work.reopen_pull(args(body)?).await?),
2784 "convert_pull_to_draft" => reply(&work.convert_pull_to_draft(args(body)?).await?),
Pull requests from branches2785 "merge_pull" => reply(&work.merge_pull(args(body)?).await?),
2786 "list_active_pulls" => reply(&work.list_active_pulls(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2787 "by_author" => reply(&work.by_author(args(body)?).await?),
Chat controls, public profiles, shadcn selects, and no Docs tab in a project2788 "contributions" => reply(&work.contributions(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2789 "start_plan" => reply(&work.start_plan(args(body)?).await?),
2790 "report_plan" => reply(&work.report_plan(args(body)?).await?),
2791 "get_plan" => reply(&work.get_plan(args(body)?).await?),
2792 "list_plans" => reply(&work.list_plans(args(body)?).await?),
2793 "apply_plan" => reply(&work.apply_plan(args(body)?).await?),
2794 "queue_issue" => reply(&work.queue_issue(args(body)?).await?),
2795 "ready_issues" => reply(&work.ready_issues(args(body)?).await?),
2796 "list_assigned_issues" => reply(&work.list_assigned_issues(args(body)?).await?),
Pull requests from branches2797 "append_session" => reply(&work.append_session(args(body)?).await?),
2798 "read_session" => reply(&work.read_session(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2799 // Agents at work, their sessions, and memory (runs.rs, memory.rs).
2800 "open_run" => reply(&work.open_run(args(body)?).await?),
2801 "report_run" => reply(&work.report_run(args(body)?).await?),
2802 "stop_run" => reply(&work.stop_run(args(body)?).await?),
2803 "list_runs" => reply(&work.list_runs(args(body)?).await?),
2804 "get_run" => reply(&work.get_run(args(body)?).await?),
2805 "list_sessions" => reply(&work.list_sessions(args(body)?).await?),
2806 "get_session" => reply(&work.get_session(args(body)?).await?),
2807 "list_memories" => reply(&work.list_memories(args(body)?).await?),
2808 "add_memory" => reply(&work.add_memory(args(body)?).await?),
2809 "update_memory" => reply(&work.update_memory(args(body)?).await?),
2810 "delete_memory" => reply(&work.delete_memory(args(body)?).await?),
2811 "recall" => reply(&work.recall(args(body)?).await?),
2812 "memory_context" => reply(&work.memory_context(args(body)?).await?),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2813 // What agents may do in a sandbox (guardrails.rs).
2814 "get_guardrails" => reply(&work.get_guardrails(args(body)?).await?),
2815 "update_guardrails" => reply(&work.update_guardrails(args(body)?).await?),
2816 "run_guardrails" => reply(&work.run_guardrails(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2817 // Plan caps the runner applies (compute.rs).
2818 "active_agents" => reply(&work.active_agents(args(body)?).await?),
2819 "issue_spend" => reply(&work.issue_spend(args(body)?).await?),
2820 "wait_for_slot" => reply(&work.wait_for_slot(args(body)?).await?),
2821 "agent_comment" => reply(&work.agent_comment(args(body)?).await?),
2822 "add_wait" => reply(&work.add_wait(args(body)?).await?),
2823 "waiting_workspaces" => reply(&work.waiting_workspaces(args(body)?).await?),
2824 "take_wait" => reply(&work.take_wait(args(body)?).await?),
2825 // The runs whose sandboxes stop with their repository (retired.rs).
2826 "runs_in_repo" => reply(&work.runs_in_repo(args(body)?).await?),
2827 "run_cost" => reply(&work.run_cost(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2828 "start_mergecheck" => reply(&work.start_mergecheck(args(body)?).await?),
2829 "report_mergecheck" => reply(&work.report_mergecheck(args(body)?).await?),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2830 // Memory that fills itself, and its review queue (capture.rs).
2831 method if capture::METHODS.contains(&method) => capture::dispatch(&work, method, body).await,
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent2832 // @g1t in comments, and the label rule (mentions.rs).
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2833 "take_mention" => reply(&work.take_mention(args(body)?).await?),
2834 "mention_revision" => reply(&work.mention_revision(args(body)?).await?),
2835 "reply_mention" => reply(&work.reply_mention(args(body)?).await?),
2836 "get_agent_rules" => reply(&work.get_agent_rules(args(body)?).await?),
2837 "set_agent_rules" => reply(&work.set_agent_rules(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2838 // Rulesets (rulesets.rs): kept here, enforced here on merge and by
2839 // repos on push.
2840 "list_rulesets" => reply(&work.list_rulesets(args(body)?).await?),
2841 "get_ruleset" => reply(&work.get_ruleset(args(body)?).await?),
2842 "save_ruleset" => reply(&work.save_ruleset(args(body)?).await?),
2843 "delete_ruleset" => reply(&work.delete_ruleset(args(body)?).await?),
2844 "effective_rules" => reply(&work.effective_rules(args(body)?).await?),
2845 "rule_evaluations" => reply(&work.rule_evaluations(args(body)?).await?),
2846 "ref_rules" => reply(&work.ref_rules(args(body)?).await?),
2847 "record_evaluations" => reply(&work.record_evaluations(args(body)?).await?),
2848 "set_requires_pull_request" => reply(&work.set_requires_pull_request(args(body)?).await?),
Pull requests from branches2849 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents2850 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2851 served.finish_timed(answered, &work.timing)
Pull requests from branches2852}
2853
2854/// Events from the bus, delivered on this service's own queue.
2855#[event(queue)]
Events service in Rust, with RFC 3339 times and accurate push events2856async fn queue(batch: MessageBatch<Event>, env: Env, _ctx: Context) -> Result<()> {
Pull requests from branches2857 let work = service(&env)?;
2858 for message in batch.messages()? {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2859 // A workspace renamed: its agent runs and memory move to the slug it has now.
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2860 if g1t_kit::rename::on_event(&env, &env.d1("DB")?, message.body(), &[memory::RENAMED, guardrails::RENAMED, rulesets::RENAMED].concat()).await? {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2861 message.ack();
2862 continue;
2863 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2864 // A repository renamed or transferred: its runs, memory, guardrails
2865 // and runs waiting for a slot follow.
2866 if g1t_kit::transfer::on_event(&env, &env.d1("DB")?, message.body(), &[memory::TRANSFERRED, guardrails::TRANSFERRED, retired::WAITS_MOVED].concat()).await? {
2867 message.ack();
2868 continue;
2869 }
2870 // A workspace deleted: what it kept for itself goes.
2871 if g1t_kit::deleted::on_event(&env.d1("DB")?, message.body(), memory::DELETED).await? {
2872 message.ack();
2873 continue;
2874 }
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)2875 // An account purged: what it wrote shows as ghost (ghost.rs).
2876 let ghost = ghost::statements();
2877 let ghost: Vec<&str> = ghost.iter().map(String::as_str).collect();
2878 if g1t_kit::user_deleted::on_event(&env.d1("DB")?, message.body(), &ghost).await? {
2879 message.ack();
2880 continue;
2881 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2882 // A repository deleted, archived or purged, or a branch renamed (retired.rs).
2883 if work.on_retired(message.body()).await? {
2884 message.ack();
2885 continue;
2886 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2887 capture::on_event(&work, message.body()).await;
Pull requests from branches2888 work.on_event(message.body()).await?;
2889 message.ack();
2890 }
2891 Ok(())
2892}
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2893
2894/// The rules that once read a pull request's author read its owner now:
2895/// whoever asked g1t for it, or its author. For each, the person who asked
2896/// is held to what an author was, and g1t's agent (a token it works with)
2897/// gains nothing by being the author.
2898#[cfg(test)]
2899mod owner_rules {
2900 use super::*;
2901 use crate::rows::stored::{ASKER, G1T, pull};
2902 use g1t_contracts::identity::AGENT_ID;
2903
2904 const SOMEONE: &str = "usr_2";
2905
2906 #[test]
2907 fn no_self_approval() {
2908 // add_comment refuses a verdict on one that is theirs.
2909 let made = pull(G1T, Some(ASKER));
2910 assert!(made.is_owned_by(ASKER.0), "the person who asked cannot approve it");
2911 assert!(!made.is_owned_by(AGENT_ID), "g1t's review agent still gives its verdict");
2912 assert!(!made.is_owned_by(SOMEONE));
2913 }
2914
2915 #[test]
2916 fn what_an_author_could_do_without_a_role() {
2917 // manageable_pull (update, ready, close), catch_up_pull on a fork,
2918 // append_session, and steering with message_agent: theirs to do.
2919 let made = pull(G1T, Some(ASKER));
2920 assert!(made.is_owned_by(ASKER.0));
2921 assert!(!made.is_owned_by(SOMEONE), "anyone else still needs the role");
2922 assert!(!made.is_owned_by(AGENT_ID), "being its author gives g1t's tokens nothing more");
2923 }
2924
2925 #[test]
2926 fn nobody_is_asked_to_review_what_they_asked_for() {
2927 // update_pull drops the owner from the reviewers asked.
2928 let made = pull(G1T, Some(ASKER));
2929 let mut reviewers = vec!["syntaqx".to_owned(), "ana".to_owned()];
2930 reviewers.retain(|name| *name != made.owner().username);
2931 assert_eq!(reviewers, ["ana"]);
2932 }
2933
2934 #[test]
2935 fn sandboxes_act_as_whoever_asked() {
2936 // LifecycleJob, ReviewJob, MergecheckJob and the merge queue's job
2937 // carry who the sandbox's credential acts for: a real account.
2938 let made = pull(G1T, Some(ASKER));
2939 assert_eq!(made.owner().id, ASKER.0);
2940 let acts_as = made.requested_by.unwrap_or(made.author);
2941 assert_eq!(acts_as.id, ASKER.0);
2942 // g1t's own work, which nobody asked for, acts as g1t, as before.
2943 let own = pull(("g1t", "g1t"), None);
2944 assert_eq!(own.requested_by.unwrap_or(own.author).id, "g1t");
2945 }
2946
2947 #[test]
2948 fn events_name_g1t_and_whoever_asked() {
2949 let made = pull(G1T, Some(ASKER));
2950 let event = serde_json::to_value(Work::pull_event(&made)).unwrap();
2951 assert_eq!(event["author"], serde_json::json!({ "id": AGENT_ID, "username": "g1t" }));
2952 assert_eq!(event["requestedBy"], serde_json::json!({ "id": "usr_1", "username": "syntaqx" }));
2953 let own = serde_json::to_value(Work::pull_event(&pull(ASKER, None))).unwrap();
2954 assert!(own.get("requestedBy").is_none());
2955 }
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts2956
2957 #[test]
2958 fn only_a_closed_pull_request_reopens_and_only_an_open_one_turns_draft() {
2959 assert_eq!(reopen_refusal(PullStatus::Closed), None);
2960 assert!(reopen_refusal(PullStatus::Merged).is_some(), "a merge cannot be undone");
2961 assert!(reopen_refusal(PullStatus::Open).is_some());
2962 assert!(reopen_refusal(PullStatus::Draft).is_some());
2963 assert_eq!(draft_refusal(PullStatus::Open), None);
2964 assert!(draft_refusal(PullStatus::Draft).is_some());
2965 assert!(draft_refusal(PullStatus::Closed).is_some());
2966 assert!(draft_refusal(PullStatus::Merged).is_some());
2967 }
2968
2969 #[test]
2970 fn a_pull_request_reopens_as_what_it_was_closed_as() {
2971 assert_eq!(reopened_status(Some("draft")), PullStatus::Draft);
2972 assert_eq!(reopened_status(Some("open")), PullStatus::Open);
2973 // Closed before this was recorded, or by a merge of another.
2974 assert_eq!(reopened_status(None), PullStatus::Open);
2975 }
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2976}

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