Skip to content

g1t/services/work/src/lib.rs

2,626 lines109,581 bytesCodeBlame

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.

Issues and pull requests replace intents and attempts1//! The work service: issues, pull requests, comments and sessions.
Work service in Rust, with RFC 3339 timestamps2//!
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
Agents and memory, checks and conflicts, profiles, slug renames, custom domains7mod authored;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API8mod capture;
Acceptance checks in sandboxes, line comments and review verdicts9mod checks;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar10mod codeowners;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look11mod compute;
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step12mod confidence;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API13mod guardrails;
Inbox: the events service tells people what needs them as events arrive14mod inbox;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar15mod labels;
Agents as a team: lifecycle, merge queue, billing and a new shell16mod lifecycle;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains17mod memory;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API18mod mentions;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains19mod mergeability;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar20mod milestones;
Agents as a team: lifecycle, merge queue, billing and a new shell21mod plans;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request22mod messages;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily23mod prefetch;
Agents as a team: lifecycle, merge queue, billing and a new shell24mod queue;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look25mod retired;
Agents as a team: lifecycle, merge queue, billing and a new shell26mod reviews;
Work service in Rust, with RFC 3339 timestamps27mod rows;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge28mod rulesets;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains29mod runs;
Agents as a team: lifecycle, merge queue, billing and a new shell30mod settings;
GitHub Actions on g1t, part two: running workflows31mod statuses;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar32mod team_reviews;
Work service in Rust, with RFC 3339 timestamps33
34use g1t_contracts::events::{
Events service in Rust, with RFC 3339 times and accurate push events35 CommentCreated, Event, IssueEvent, NewEvent, Publish, PullEvent, SessionAppended,
Work service in Rust, with RFC 3339 timestamps36};
Agents as a team: lifecycle, merge queue, billing and a new shell37use g1t_contracts::identity::UsernameArgs;
Catching up with main takes seconds when the two sides touched different files38use g1t_contracts::repos::{
Fast pages, required checks on the branch, self-hosted runners, honest incidents39 ForkArgs, GetArgs, HeadArgs, LandArgs, Landed, NeedsAgentReason, PullBranchUpdate, ReadableArgs, Repo, RepoPath,
Catching up with main takes seconds when the two sides touched different files40 UpdatePullBranchArgs,
41};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look42use g1t_contracts::access::{self, Capability, Denied};
Work service in Rust, with RFC 3339 timestamps43use g1t_contracts::time::rfc3339;
44use g1t_contracts::work::*;
Agents as a team: lifecycle, merge queue, billing and a new shell45use futures_util::future::{try_join, try_join3, try_join_all};
Work service in Rust, with RFC 3339 timestamps46use g1t_contracts::{FailureCode, Outcome, User, Viewer, new_id};
Events service in Rust, with RFC 3339 times and accurate push events47use g1t_kit::{args, now_ms, reply, rpc_method};
Work service in Rust, with RFC 3339 timestamps48use serde::Serialize;
49use worker::wasm_bindgen::JsValue;
50use worker::{
51 Context, D1Database, Env, Fetcher, MessageBatch, MessageExt, Request, Response, Result, event,
52};
53
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look54use retired::writable;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar55use rows::{CommentRow, IssueRow, MovedRow, NumberRow, PULL_COLUMNS, PullRow, SessionRow, Snapshot};
Work service in Rust, with RFC 3339 timestamps56
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge57pub(crate) const SOURCE: &str = "work";
Work service in Rust, with RFC 3339 timestamps58const MAX_ENTRY_BATCH: usize = 200;
59const MAX_ENTRY_CHARS: usize = 64_000;
Issues and pull requests replace intents and attempts60const MAX_TITLE_CHARS: usize = 200;
Work service in Rust, with RFC 3339 timestamps61const SESSION_PAGE: u32 = 500;
Issues and pull requests replace intents and attempts62const LIST_PAGE: u32 = 100;
Agents as a team: lifecycle, merge queue, billing and a new shell63const MAX_ASSIGNEES: usize = 10;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge64pub(crate) const UNVERIFIED: &str = "Confirm your email address first. Check your inbox, or resend the link from the banner on g1t.sh.";
Work service in Rust, with RFC 3339 timestamps65
Issues and pull requests replace intents and attempts66const ISSUE_COLUMNS: &str = "issues.*,
67 (SELECT count(*) FROM pulls WHERE pulls.issue_id = issues.id) AS pull_count,
Agents as a team: lifecycle, merge queue, billing and a new shell68 (SELECT agent FROM pulls
69 WHERE pulls.issue_id = issues.id AND pulls.status IN ('draft', 'open')
70 AND pulls.fork_repo_id IS NOT NULL
71 ORDER BY pulls.number DESC LIMIT 1) AS agent,
Issues and pull requests replace intents and attempts72 (SELECT count(*) FROM comments
Agents as a team: lifecycle, merge queue, billing and a new shell73 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 bar74 AND comments.kind = 'comment') AS comment_count,
75 (SELECT title FROM milestones
76 WHERE milestones.repo_id = issues.repo_id AND milestones.number = issues.milestone) AS milestone_title";
Work service in Rust, with RFC 3339 timestamps77
Issues and pull requests replace intents and attempts78fn no_issue<T>() -> Outcome<T> {
79 Outcome::fail(FailureCode::NotFound, "Issue not found.")
Work service in Rust, with RFC 3339 timestamps80}
81
Issues and pull requests replace intents and attempts82fn no_pull<T>() -> Outcome<T> {
83 Outcome::fail(FailureCode::NotFound, "Pull request not found.")
Work service in Rust, with RFC 3339 timestamps84}
85
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily86/// Whether a pull request was behind when its mergeability was last
87/// worked out, and for which pair of commits (mergeability.rs).
88#[derive(serde::Deserialize)]
89struct StoredBehind {
90 #[serde(default)]
91 behind: Option<u8>,
92 #[serde(default)]
93 mergeable_key: Option<String>,
94}
95
96impl StoredBehind {
97 /// The stored answer, if it was worked out for `head`.
98 fn for_head(&self, head: Option<&str>) -> Option<bool> {
99 let (worked_for, _) = self.mergeable_key.as_deref()?.split_once("..")?;
100 (Some(worked_for) == head).then_some(self.behind? != 0)
101 }
102}
103
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look104/// Refuses `actor` unless their role on `repo` has `capability`: not found
105/// when they cannot read it, forbidden with the role it needs otherwise.
106pub(crate) fn allowed(actor: Option<&User>, repo: &Repo, capability: Capability) -> Outcome<()> {
107 match access::check(actor, repo, capability) {
108 Ok(()) => Outcome::Ok(()),
109 Err(Denied::NotFound) => Outcome::fail(FailureCode::NotFound, "Repository not found."),
110 Err(Denied::Forbidden) => Outcome::fail(
111 FailureCode::Forbidden,
112 access::needs(capability, &format!("{}/{}", repo.namespace, repo.name)),
113 ),
114 }
115}
116
Work service in Rust, with RFC 3339 timestamps117fn optional(value: &Option<String>) -> JsValue {
118 value.as_deref().map_or(JsValue::NULL, JsValue::from)
119}
120
Issues and pull requests replace intents and attempts121fn optional_number(value: Option<u32>) -> JsValue {
122 value.map_or(JsValue::NULL, JsValue::from)
123}
124
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar125/// Names the branch a pull request merges into when it is the default
126/// branch, which is stored as none so that it follows a change of default.
127pub(crate) fn fill_base(pull: &mut Pull, repo: &Repo) {
128 if pull.base.as_deref().is_none_or(str::is_empty) {
129 pull.base = Some(repo.default_branch.clone());
130 }
131}
132
133/// The branch a pull request is stored as merging into: none for the
134/// default branch.
135fn stored_base(base: &str, repo: &Repo) -> Option<String> {
136 let base = base.trim();
137 (!base.is_empty() && base != repo.default_branch).then(|| base.to_owned())
138}
139
Issues and pull requests replace intents and attempts140/// The lowercase name a `State` is stored and sent as.
141fn state_name(state: Option<State>) -> Option<&'static str> {
142 state.map(|state| match state {
143 State::Open => "open",
144 State::Closed => "closed",
145 })
146}
147
148/// A trimmed title, or why it cannot be used.
149fn valid_title(title: &str) -> std::result::Result<&str, &'static str> {
150 let title = title.trim();
151 if title.is_empty() {
152 Err("A title is required.")
153 } else if title.chars().count() > MAX_TITLE_CHARS {
154 Err("That title is too long.")
155 } else {
156 Ok(title)
157 }
158}
159
160/// Unwraps an `Outcome`, returning its failure from the enclosing method.
161macro_rules! check {
162 ($outcome:expr) => {
163 match $outcome {
164 Outcome::Ok(value) => value,
165 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
166 }
167 };
168}
169
Work service in Rust, with RFC 3339 timestamps170struct Work {
171 db: D1Database,
Agents as a team: lifecycle, merge queue, billing and a new shell172 identity: Fetcher,
Work service in Rust, with RFC 3339 timestamps173 repos: Fetcher,
Events service in Rust, with RFC 3339 times and accurate push events174 events: Fetcher,
Sidebar: the panels really slide175 /// GitHub Actions: runs a merge queue's `merge_group` workflows.
176 actions: Fetcher,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily177 /// Where this request's time went, for its `Server-Timing`.
178 timing: g1t_kit::d1::Timing,
179 /// A pull request's rows read in one batch for this request
180 /// (prefetch.rs), which the helpers below read instead of the database.
181 prefetched: std::cell::RefCell<Option<std::rc::Rc<prefetch::Prefetched>>>,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge182 /// Repositories read in this request, by id, for rules that need one
183 /// where only a pull request is at hand (rulesets.rs `repo_for`).
184 known_repos: std::cell::RefCell<std::collections::HashMap<String, Repo>>,
Work service in Rust, with RFC 3339 timestamps185}
186
187impl Work {
Issues and pull requests replace intents and attempts188 async fn publish<T: Serialize>(
189 &self,
190 kind: &'static str,
191 repo_id: &str,
192 actor: &User,
193 data: T,
194 ) -> Result<()> {
Acceptance checks in sandboxes, line comments and review verdicts195 self.publish_as(kind, repo_id, Some(actor.id.clone()), data)
196 .await
197 }
198
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights199 /// A pull request's owner (whoever asked g1t for it, or its author) as
200 /// a viewer who can read its repository and source. Stored people carry
201 /// no memberships, so a private repository would otherwise look missing
202 /// to them. The membership given reads and nothing more: it is for
203 /// looking, never for acting.
204 pub(crate) async fn owner_viewer(&self, pull: &Pull) -> Result<Viewer> {
Agents move along on private repositories too205 let path: Option<RepoPath> = g1t_kit::call(
206 &self.repos,
207 "path_by_id",
208 &g1t_contracts::repos::PathByIdArgs { id: pull.repo_id.clone() },
209 )
210 .await?;
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights211 let mut owner = pull.owner().clone();
Agents move along on private repositories too212 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 rights213 && !owner.is_member(&path.namespace.to_lowercase())
Agents move along on private repositories too214 {
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights215 owner.workspaces.push(g1t_contracts::Membership {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look216 base_permission: Some(access::BasePermission::Read),
217 ..g1t_contracts::Membership::member(path.namespace.to_lowercase())
218 });
Agents move along on private repositories too219 }
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights220 Ok(Some(owner))
Agents move along on private repositories too221 }
222
Acceptance checks in sandboxes, line comments and review verdicts223 /// Publishes an event caused by `actor`, or by g1t itself.
224 async fn publish_as<T: Serialize>(
225 &self,
226 kind: &'static str,
227 repo_id: &str,
228 actor: Option<String>,
229 data: T,
230 ) -> Result<()> {
Issues and pull requests replace intents and attempts231 let event = NewEvent {
232 kind,
233 source: SOURCE,
234 repo_id: Some(repo_id.to_owned()),
Acceptance checks in sandboxes, line comments and review verdicts235 actor,
Issues and pull requests replace intents and attempts236 data,
237 };
Events service in Rust, with RFC 3339 times and accurate push events238 g1t_kit::call(
239 &self.events,
240 "publish",
241 &Publish {
242 events: vec![event],
243 },
244 )
245 .await
Work service in Rust, with RFC 3339 timestamps246 }
247
Issues and pull requests replace intents and attempts248 /// The repository, if the viewer may see it. Whether they may is
249 /// decided by the repos service.
250 async fn repo(&self, path: &RepoPath, viewer: &Viewer) -> Result<Outcome<Repo>> {
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge251 let found: Outcome<Repo> = self
252 .timing
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily253 .rpc(g1t_kit::call(
254 &self.repos,
255 "get",
256 &GetArgs {
257 path: path.clone(),
258 viewer: viewer.clone(),
259 },
260 ))
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge261 .await?;
262 if let Outcome::Ok(repo) = &found {
263 self.known_repos.borrow_mut().insert(repo.id.clone(), repo.clone());
264 }
265 Ok(found)
Work service in Rust, with RFC 3339 timestamps266 }
267
Issues and pull requests replace intents and attempts268 /// The next number in the repository's sequence. Taking it is one
269 /// statement, so concurrent opens cannot be given the same number.
270 async fn next_number(&self, repo_id: &str) -> Result<u32> {
271 let row = self
272 .db
273 .prepare(
274 "INSERT INTO counters (repo_id, last) VALUES (?, 1)
275 ON CONFLICT (repo_id) DO UPDATE SET last = last + 1
276 RETURNING last AS n",
277 )
278 .bind(&[repo_id.into()])?
279 .first::<NumberRow>(None)
280 .await?;
281 row.map(|row| row.n)
282 .ok_or_else(|| worker::Error::RustError("no number was assigned".into()))
Work service in Rust, with RFC 3339 timestamps283 }
284
Issues and pull requests replace intents and attempts285 async fn issue(&self, repo_id: &str, number: u32) -> Result<Option<Issue>> {
Work service in Rust, with RFC 3339 timestamps286 Ok(self
287 .db
Issues and pull requests replace intents and attempts288 .prepare(format!(
289 "SELECT {ISSUE_COLUMNS} FROM issues WHERE repo_id = ? AND number = ?"
290 ))
291 .bind(&[repo_id.into(), number.into()])?
292 .first::<IssueRow>(None)
Work service in Rust, with RFC 3339 timestamps293 .await?
Issues and pull requests replace intents and attempts294 .map(Issue::from))
Work service in Rust, with RFC 3339 timestamps295 }
296
Issues and pull requests replace intents and attempts297 async fn pull(&self, repo_id: &str, number: u32) -> Result<Option<Pull>> {
Work service in Rust, with RFC 3339 timestamps298 Ok(self
299 .db
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step300 .prepare(format!("SELECT {PULL_COLUMNS} FROM pulls WHERE repo_id = ? AND number = ?"))
Issues and pull requests replace intents and attempts301 .bind(&[repo_id.into(), number.into()])?
302 .first::<PullRow>(None)
303 .await?
304 .map(Pull::from))
Work service in Rust, with RFC 3339 timestamps305 }
306
Issues and pull requests replace intents and attempts307 /// The repository and one of its issues, as seen by `viewer`.
308 async fn issue_at(
309 &self,
310 path: &RepoPath,
311 number: u32,
312 viewer: &Viewer,
313 ) -> Result<Outcome<(Repo, Issue)>> {
314 let Outcome::Ok(repo) = self.repo(path, viewer).await? else {
315 return Ok(no_issue());
Work service in Rust, with RFC 3339 timestamps316 };
Issues and pull requests replace intents and attempts317 Ok(match self.issue(&repo.id, number).await? {
318 Some(issue) => Outcome::Ok((repo, issue)),
319 None => no_issue(),
320 })
321 }
322
323 /// The repository and one of its pull requests, as seen by `viewer`.
324 async fn pull_at(
325 &self,
326 path: &RepoPath,
327 number: u32,
328 viewer: &Viewer,
329 ) -> Result<Outcome<(Repo, Pull)>> {
330 let Outcome::Ok(repo) = self.repo(path, viewer).await? else {
331 return Ok(no_pull());
332 };
333 Ok(match self.pull(&repo.id, number).await? {
334 Some(pull) => Outcome::Ok((repo, pull)),
335 None => no_pull(),
336 })
337 }
338
Agents as a team: lifecycle, merge queue, billing and a new shell339 /// Records something that happened to an issue or a pull request, so
340 /// that it shows in the conversation where it happened. `text` is what
341 /// `author` did, as the rest of a sentence starting with their name.
342 pub(crate) async fn note(
343 &self,
344 repo_id: &str,
345 number: u32,
346 author: (&str, &str),
347 text: &str,
348 ) -> Result<()> {
349 let now = now_ms();
350 self.db
351 .prepare(
352 "INSERT INTO comments
353 (id, repo_id, number, author_id, author_name, body, kind, created_at)
354 VALUES (?, ?, ?, ?, ?, ?, 'event', ?)",
355 )
356 .bind(&[
357 new_id("cmt", now).into(),
358 repo_id.into(),
359 number.into(),
360 author.0.into(),
361 author.1.into(),
362 text.into(),
363 rfc3339(now).into(),
364 ])?
365 .run()
366 .await?;
367 Ok(())
368 }
369
370 /// 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-agent371 /// "assigned ana" or "requested a review from g1t".
Agents as a team: lifecycle, merge queue, billing and a new shell372 async fn note_changes(
373 &self,
374 repo_id: &str,
375 number: u32,
376 actor: &User,
377 before: &[String],
378 after: &[String],
379 (added, removed): (&str, &str),
380 ) -> Result<()> {
381 let joined = |names: Vec<&String>| {
382 names
383 .into_iter()
384 .map(String::as_str)
385 .collect::<Vec<_>>()
386 .join(", ")
387 };
388 let new: Vec<&String> = after.iter().filter(|name| !before.contains(name)).collect();
389 let gone: Vec<&String> = before.iter().filter(|name| !after.contains(name)).collect();
390 let who = (actor.id.as_str(), actor.username.as_str());
391 if !new.is_empty() {
392 // Taking something on oneself reads better said that way.
393 let text = if added == "assigned" && new == [&actor.username] {
394 "self-assigned this".to_owned()
395 } else {
396 format!("{added} {}", joined(new))
397 };
398 self.note(repo_id, number, who, &text).await?;
399 }
400 if !gone.is_empty() {
401 self.note(repo_id, number, who, &format!("{removed} {}", joined(gone)))
402 .await?;
403 }
404 Ok(())
405 }
406
Issues and pull requests replace intents and attempts407 fn issue_event(issue: &Issue) -> IssueEvent {
408 IssueEvent {
409 issue_id: issue.id.clone(),
410 repo_id: issue.repo_id.clone(),
411 number: issue.number,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights412 author: Some((&issue.author).into()),
413 requested_by: issue.requested_by.as_ref().map(Into::into),
Issues and pull requests replace intents and attempts414 ..IssueEvent::default()
Work service in Rust, with RFC 3339 timestamps415 }
416 }
417
Workflows run when an agent's pull request is marked ready418 /// The commit a pull request's change is at in git right now: its
419 /// fork's default branch, or its branch.
420 async fn live_head(&self, pull: &Pull) -> Result<Option<String>> {
421 g1t_kit::call(
422 &self.repos,
423 "head",
424 &HeadArgs {
425 repo_id: pull.fork_repo_id.clone().unwrap_or_else(|| pull.repo_id.clone()),
426 branch: pull.branch.clone().unwrap_or_default(),
427 },
428 )
429 .await
430 }
431
Issues and pull requests replace intents and attempts432 fn pull_event(pull: &Pull) -> PullEvent {
433 PullEvent {
434 pull_id: pull.id.clone(),
435 repo_id: pull.repo_id.clone(),
436 number: pull.number,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights437 author: Some((&pull.author).into()),
438 requested_by: pull.requested_by.as_ref().map(Into::into),
Issues and pull requests replace intents and attempts439 issue: pull.issue,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step440 confidence: pull.confidence.clone(),
Issues and pull requests replace intents and attempts441 ..PullEvent::default()
Work service in Rust, with RFC 3339 timestamps442 }
443 }
444
Issues and pull requests replace intents and attempts445 // --- Issues ------------------------------------------------------------
446
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent447 /// 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 step448 /// anything is opened unless the actor may put agents to work here. The
449 /// runner's `delegate` starts the agent on it.
450 async fn delegate_issue(&self, a: DelegateIssueArgs) -> Result<Outcome<Issue>> {
451 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
452 check!(writable(&repo));
453 check!(allowed(Some(&a.actor), &repo, Capability::Run));
454 self.open_issue(OpenIssueArgs {
455 actor: a.actor,
456 repo: a.repo,
457 title: a.title,
458 body: a.body,
459 labels: a.labels,
460 checks: a.checks,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar461 milestone: None,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step462 })
463 .await
464 }
465
Issues and pull requests replace intents and attempts466 async fn open_issue(&self, a: OpenIssueArgs) -> Result<Outcome<Issue>> {
Work service in Rust, with RFC 3339 timestamps467 if !a.actor.verified {
468 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
469 }
Issues and pull requests replace intents and attempts470 let title = match valid_title(&a.title) {
471 Ok(title) => title,
472 Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)),
473 };
474 let Some(labels) = normalize_labels(&a.labels) else {
Work service in Rust, with RFC 3339 timestamps475 return Ok(Outcome::fail(
476 FailureCode::Invalid,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar477 format!("An issue can have up to {MAX_LABELS} labels of up to {MAX_LABEL_CHARS} characters each."),
Work service in Rust, with RFC 3339 timestamps478 ));
479 };
Issues and pull requests replace intents and attempts480 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 look481 check!(writable(&repo));
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar482 // Labels it does not have yet are made for someone who may triage;
483 // anyone else picks from those there are.
484 let colors = check!(self.ensure_labels(&a.actor, &repo, &labels).await?);
485 let milestone = match a.milestone.filter(|number| *number > 0) {
486 Some(number) => {
487 check!(allowed(Some(&a.actor), &repo, Capability::Triage));
488 check!(self.milestone_ref(&repo.id, number).await?)
489 }
490 None => None,
491 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents492 // Commands given the old way are words for the agent now: added to
493 // the body under "Definition of done". What has to pass to merge is
494 // the branch's required checks.
495 let body = with_definition_of_done(&a.body, &commands_pass(&a.checks));
Work service in Rust, with RFC 3339 timestamps496
497 let now = now_ms();
Issues and pull requests replace intents and attempts498 let id = new_id("iss", now);
499 let number = self.next_number(&repo.id).await?;
500 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 rights501 // What g1t's agent files at work is g1t's, for the person it works for.
502 let (author, requested_by) = authorship(&a.actor, false);
Work service in Rust, with RFC 3339 timestamps503 self.db
504 .prepare(
Issues and pull requests replace intents and attempts505 "INSERT INTO issues
506 (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 bar507 requested_by_id, requested_by_name, created_at, updated_at, milestone)
508 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Work service in Rust, with RFC 3339 timestamps509 )
510 .bind(&[
511 id.as_str().into(),
512 repo.id.as_str().into(),
Issues and pull requests replace intents and attempts513 number.into(),
Work service in Rust, with RFC 3339 timestamps514 title.into(),
Fast pages, required checks on the branch, self-hosted runners, honest incidents515 body.into(),
Issues and pull requests replace intents and attempts516 serde_json::to_string(&labels)?.into(),
Fast pages, required checks on the branch, self-hosted runners, honest incidents517 "[]".into(),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights518 author.id.as_str().into(),
519 author.username.as_str().into(),
520 optional(&requested_by.as_ref().map(|user| user.id.clone())),
521 optional(&requested_by.as_ref().map(|user| user.username.clone())),
Issues and pull requests replace intents and attempts522 timestamp.as_str().into(),
523 timestamp.as_str().into(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar524 optional_number(milestone.as_ref().map(|milestone| milestone.number)),
Work service in Rust, with RFC 3339 timestamps525 ])?
526 .run()
527 .await?;
Issues and pull requests replace intents and attempts528 let Some(issue) = self.issue(&repo.id, number).await? else {
529 return Ok(no_issue());
Work service in Rust, with RFC 3339 timestamps530 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API531 self.apply_label_rule(&a.actor, &issue, &[]).await?;
Issues and pull requests replace intents and attempts532 self.publish(
533 "issue.opened",
534 &repo.id,
535 &a.actor,
536 IssueEvent {
537 title: Some(issue.title.clone()),
538 ..Self::issue_event(&issue)
Work service in Rust, with RFC 3339 timestamps539 },
Issues and pull requests replace intents and attempts540 )
Work service in Rust, with RFC 3339 timestamps541 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar542 // Opened with labels and a milestone: each is said, as it would be
543 // if they were added afterwards, without notes in the conversation.
544 for label in &issue.labels {
545 let color = colors.iter().find(|(name, _)| name == label).map_or_else(|| label_color_for(label), |(_, c)| c.clone());
546 let label = Some(g1t_contracts::events::EventLabel { name: label.clone(), color });
547 self.publish("issue.labeled", &repo.id, &a.actor, IssueEvent { label, ..Self::issue_event(&issue) })
548 .await?;
549 }
550 if let Some(milestone) = milestone {
551 self.publish(
552 "issue.milestoned",
553 &repo.id,
554 &a.actor,
555 IssueEvent { milestone: Some(milestone), ..Self::issue_event(&issue) },
556 )
557 .await?;
558 }
Issues and pull requests replace intents and attempts559 Ok(Outcome::Ok(issue))
Work service in Rust, with RFC 3339 timestamps560 }
561
Issues and pull requests replace intents and attempts562 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 daily563 let state = state_name(a.state);
Issues and pull requests replace intents and attempts564 let label = a
565 .label
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar566 .map(|label| label.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase())
Issues and pull requests replace intents and attempts567 .filter(|label| !label.is_empty());
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar568 let milestone = a.milestone;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily569 let list = |repo_id: String| {
570 let label = label.clone();
571 async move {
572 let state = state.map_or(JsValue::NULL, JsValue::from);
573 let query = self
574 .db
575 .prepare(format!(
576 "SELECT {ISSUE_COLUMNS} FROM issues
577 WHERE repo_id = ? AND (? IS NULL OR state = ?)
578 AND (? IS NULL OR EXISTS
579 (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 bar580 AND (? IS NULL OR milestone = ?)
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily581 ORDER BY number DESC LIMIT ?"
582 ))
583 .bind(&[
584 repo_id.into(),
585 state.clone(),
586 state,
587 optional(&label),
588 optional(&label),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar589 optional_number(milestone),
590 optional_number(milestone),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily591 LIST_PAGE.into(),
592 ])?;
593 self.timing.db(1, query.all()).await?.results::<IssueRow>()
594 }
595 };
596 let (_, rows) = check!(self.repo_then(&a.repo, &a.viewer, list).await?);
Issues and pull requests replace intents and attempts597 Ok(Outcome::Ok(rows.into_iter().map(Issue::from).collect()))
Work service in Rust, with RFC 3339 timestamps598 }
599
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily600 /// An issue, the pull requests for it and its comments: one batch,
601 /// started beside the access check (prefetch.rs).
Issues and pull requests replace intents and attempts602 async fn get_issue(&self, a: ViewArgs) -> Result<Outcome<IssueDetail>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily603 let number = a.number;
604 let read = |repo_id: String| async move {
605 let key = [JsValue::from(repo_id.as_str()), JsValue::from(number)];
606 let statements = vec![
607 self.db
608 .prepare(format!("SELECT {ISSUE_COLUMNS} FROM issues WHERE repo_id = ?1 AND number = ?2"))
609 .bind(&key)?,
610 self.db
611 .prepare(format!(
612 "SELECT {PULL_COLUMNS} FROM pulls
613 WHERE issue_id = (SELECT id FROM issues WHERE repo_id = ?1 AND number = ?2)
614 ORDER BY number"
615 ))
616 .bind(&key)?,
617 self.db
618 .prepare("SELECT * FROM comments WHERE repo_id = ?1 AND number = ?2 ORDER BY id LIMIT 500")
619 .bind(&key)?,
620 ];
621 let results = self.timing.db(3, self.db.batch(statements)).await?;
622 let rows = |index: usize| results.get(index).ok_or_else(|| worker::Error::RustError("short batch".into()));
623 Ok::<_, worker::Error>((
624 rows(0)?.results::<IssueRow>()?.into_iter().next().map(Issue::from),
625 rows(1)?.results::<PullRow>()?.into_iter().map(Pull::from).collect::<Vec<_>>(),
626 rows(2)?.results::<CommentRow>()?.into_iter().map(Comment::from).collect::<Vec<_>>(),
627 ))
628 };
629 let Outcome::Ok((_, (Some(issue), pulls, comments))) = self.repo_then(&a.repo, &a.viewer, read).await? else {
630 return Ok(no_issue());
631 };
632 Ok(Outcome::Ok(IssueDetail { comments, pulls, issue }))
Work service in Rust, with RFC 3339 timestamps633 }
634
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look635 /// The issue, if `actor` wrote it or may triage the repository's issues.
Issues and pull requests replace intents and attempts636 async fn manageable_issue(
637 &self,
638 actor: &User,
639 path: &RepoPath,
640 number: u32,
641 ) -> Result<Outcome<Issue>> {
642 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 look643 check!(writable(&repo));
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights644 if issue.owner().id != actor.id {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look645 check!(allowed(Some(actor), &repo, Capability::Triage));
Work service in Rust, with RFC 3339 timestamps646 }
Issues and pull requests replace intents and attempts647 Ok(Outcome::Ok(issue))
648 }
649
650 async fn update_issue(&self, a: UpdateIssueArgs) -> Result<Outcome<Issue>> {
651 let issue = check!(self.manageable_issue(&a.actor, &a.repo, a.number).await?);
652 let title = match a.title.as_deref().map(valid_title) {
653 Some(Err(message)) => return Ok(Outcome::fail(FailureCode::Invalid, message)),
654 Some(Ok(title)) => Some(title.to_owned()),
655 None => None,
656 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar657 if a.labels.as_deref().is_some_and(|labels| normalize_labels(labels).is_none()) {
658 return Ok(Outcome::fail(
659 FailureCode::Invalid,
660 format!("An issue can have up to {MAX_LABELS} labels of up to {MAX_LABEL_CHARS} characters each."),
661 ));
662 }
Agents as a team: lifecycle, merge queue, billing and a new shell663 let assignees = match a.assignees {
664 Some(names) => Some(check!(self.valid_assignees(names).await?)),
665 None => None,
666 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar667 // Its labels and milestone first: either can be refused, and then
668 // nothing else changes.
669 if a.milestone.is_some() || a.labels.is_some() {
670 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
671 if let Some(number) = a.milestone {
672 check!(self.set_milestone(&a.actor, &repo, &labels::Item::Issue(issue.clone()), number).await?);
673 }
674 if let Some(labels) = &a.labels {
675 check!(self.relabel(&a.actor, &repo, &labels::Item::Issue(issue.clone()), labels).await?);
676 }
677 }
Agents as a team: lifecycle, merge queue, billing and a new shell678 let assigned = assignees.as_ref().map(serde_json::to_string).transpose()?;
Issues and pull requests replace intents and attempts679 let body = a.body.map(|body| body.trim().to_owned());
680 self.db
681 .prepare(
682 "UPDATE issues
683 SET title = COALESCE(?, title), body = COALESCE(?, body),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar684 assignees = COALESCE(?, assignees), updated_at = ?
Issues and pull requests replace intents and attempts685 WHERE id = ?",
686 )
687 .bind(&[
688 optional(&title),
689 optional(&body),
Agents as a team: lifecycle, merge queue, billing and a new shell690 optional(&assigned),
Issues and pull requests replace intents and attempts691 rfc3339(now_ms()).into(),
692 issue.id.as_str().into(),
693 ])?
694 .run()
695 .await?;
Agents as a team: lifecycle, merge queue, billing and a new shell696 let before = issue.assignees.clone();
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API697 let labels_before = issue.labels.clone();
Issues and pull requests replace intents and attempts698 let Some(issue) = self.issue(&issue.repo_id, issue.number).await? else {
699 return Ok(no_issue());
700 };
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API701 self.apply_label_rule(&a.actor, &issue, &labels_before).await?;
Issues and pull requests replace intents and attempts702 self.publish(
703 "issue.updated",
704 &issue.repo_id,
705 &a.actor,
706 Self::issue_event(&issue),
707 )
708 .await?;
Agents as a team: lifecycle, merge queue, billing and a new shell709 if let Some(assignees) = assignees {
710 self.note_changes(
711 &issue.repo_id,
712 issue.number,
713 &a.actor,
714 &before,
715 &assignees,
716 ("assigned", "unassigned"),
717 )
718 .await?;
Events: review requests, assignments, stops and deployments are published719 let added: Vec<String> = assignees.iter().filter(|name| !before.contains(name)).cloned().collect();
Agents as a team: lifecycle, merge queue, billing and a new shell720 self.publish(
721 "issue.assigned",
722 &issue.repo_id,
723 &a.actor,
724 IssueEvent {
725 assignees: Some(assignees),
Events: review requests, assignments, stops and deployments are published726 added: Some(added),
Agents as a team: lifecycle, merge queue, billing and a new shell727 ..Self::issue_event(&issue)
728 },
729 )
730 .await?;
731 }
Issues and pull requests replace intents and attempts732 Ok(Outcome::Ok(issue))
733 }
734
Agents as a team: lifecycle, merge queue, billing and a new shell735 /// Usernames as given, tidied, if each names an account.
736 async fn valid_assignees(&self, names: Vec<String>) -> Result<Outcome<Vec<String>>> {
737 let mut assignees: Vec<String> = Vec::new();
738 for name in names {
739 let name = name.trim().trim_start_matches('@').to_lowercase();
740 if name.is_empty() || assignees.contains(&name) {
741 continue;
742 }
743 if assignees.len() == MAX_ASSIGNEES {
744 return Ok(Outcome::fail(
745 FailureCode::Invalid,
746 format!("An issue can be assigned to at most {MAX_ASSIGNEES} people."),
747 ));
748 }
749 let account: Viewer = g1t_kit::call(
750 &self.identity,
751 "user_by_username",
752 &UsernameArgs {
753 username: name.clone(),
754 },
755 )
756 .await?;
757 if account.is_none() {
758 return Ok(Outcome::fail(
759 FailureCode::Invalid,
760 format!("There is no account named {name}."),
761 ));
762 }
763 assignees.push(name);
764 }
765 Ok(Outcome::Ok(assignees))
766 }
767
768 /// Open issues assigned to the viewer, in every repository. Callers
769 /// show only those in repositories the viewer can still see.
770 async fn list_assigned_issues(&self, a: ViewerArgs) -> Result<Vec<Issue>> {
771 let Some(viewer) = a.viewer else {
772 return Ok(Vec::new());
773 };
774 let rows = self
775 .db
776 .prepare(format!(
777 "SELECT {ISSUE_COLUMNS} FROM issues
778 WHERE state = 'open' AND EXISTS (
779 SELECT 1 FROM json_each(issues.assignees) WHERE json_each.value = ?)
780 ORDER BY updated_at DESC LIMIT 50"
781 ))
782 .bind(&[viewer.username.into()])?
783 .all()
784 .await?
785 .results::<IssueRow>()?;
786 Ok(rows.into_iter().map(Issue::from).collect())
787 }
788
Issues and pull requests replace intents and attempts789 async fn close_issue(&self, a: IssueActionArgs) -> Result<Outcome<Issue>> {
790 let mut issue = check!(self.manageable_issue(&a.actor, &a.repo, a.number).await?);
791 if issue.state == State::Closed {
Work service in Rust, with RFC 3339 timestamps792 return Ok(Outcome::fail(
793 FailureCode::Conflict,
Issues and pull requests replace intents and attempts794 "This issue is already closed.",
Work service in Rust, with RFC 3339 timestamps795 ));
796 }
Issues and pull requests replace intents and attempts797 let reason = a.reason.unwrap_or(IssueReason::Completed);
798 let now = rfc3339(now_ms());
Work service in Rust, with RFC 3339 timestamps799 self.db
Issues and pull requests replace intents and attempts800 .prepare(
801 "UPDATE issues SET state = 'closed', reason = ?, closed_at = ?, updated_at = ?
802 WHERE id = ?",
803 )
804 .bind(&[
805 reason.as_str().into(),
806 now.as_str().into(),
807 now.as_str().into(),
808 issue.id.as_str().into(),
809 ])?
Work service in Rust, with RFC 3339 timestamps810 .run()
811 .await?;
Issues and pull requests replace intents and attempts812 self.publish(
813 "issue.closed",
814 &issue.repo_id,
815 &a.actor,
816 IssueEvent {
817 reason: Some(reason.as_str()),
818 ..Self::issue_event(&issue)
Work service in Rust, with RFC 3339 timestamps819 },
Issues and pull requests replace intents and attempts820 )
Work service in Rust, with RFC 3339 timestamps821 .await?;
Agents as a team: lifecycle, merge queue, billing and a new shell822 self.note(
823 &issue.repo_id,
824 issue.number,
825 (&a.actor.id, &a.actor.username),
826 match reason {
827 IssueReason::Completed => "closed this as completed",
828 IssueReason::NotPlanned => "closed this as not planned",
829 },
830 )
831 .await?;
Issues and pull requests replace intents and attempts832 issue.state = State::Closed;
833 issue.reason = Some(reason);
834 issue.closed_at = Some(now.clone());
835 issue.updated_at = now;
836 Ok(Outcome::Ok(issue))
837 }
838
839 async fn reopen_issue(&self, a: IssueActionArgs) -> Result<Outcome<Issue>> {
840 let mut issue = check!(self.manageable_issue(&a.actor, &a.repo, a.number).await?);
841 if issue.state == State::Open {
842 return Ok(Outcome::fail(
843 FailureCode::Conflict,
844 "This issue is already open.",
845 ));
846 }
847 let now = rfc3339(now_ms());
848 self.db
849 .prepare(
850 "UPDATE issues
851 SET state = 'open', reason = NULL, resolved_by = NULL, closed_at = NULL,
852 updated_at = ?
853 WHERE id = ?",
854 )
855 .bind(&[now.as_str().into(), issue.id.as_str().into()])?
856 .run()
857 .await?;
858 self.publish(
859 "issue.reopened",
860 &issue.repo_id,
861 &a.actor,
862 Self::issue_event(&issue),
863 )
864 .await?;
Agents as a team: lifecycle, merge queue, billing and a new shell865 self.note(
866 &issue.repo_id,
867 issue.number,
868 (&a.actor.id, &a.actor.username),
869 "reopened this",
870 )
871 .await?;
Issues and pull requests replace intents and attempts872 issue.state = State::Open;
873 issue.reason = None;
874 issue.resolved_by = None;
875 issue.closed_at = None;
876 issue.updated_at = now;
877 Ok(Outcome::Ok(issue))
Work service in Rust, with RFC 3339 timestamps878 }
879
Issues and pull requests replace intents and attempts880
881 async fn counts(&self, a: ViewArgs) -> Result<Outcome<Counts>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily882 let read = |repo_id: String| async move {
883 let query = self
884 .db
885 .prepare(
886 "SELECT
887 (SELECT count(*) FROM issues WHERE repo_id = ?1 AND state = 'open') AS issues,
888 (SELECT count(*) FROM pulls
889 WHERE repo_id = ?1 AND status IN ('draft', 'open')) AS pulls",
890 )
891 .bind(&[repo_id.into()])?;
892 self.timing.db(1, query.first::<Counts>(None)).await
893 };
894 let (_, counts) = check!(self.repo_then(&a.repo, &a.viewer, read).await?);
Issues and pull requests replace intents and attempts895 Ok(Outcome::Ok(counts.unwrap_or(Counts {
896 issues: 0,
897 pulls: 0,
898 })))
899 }
900
901 // --- Comments ----------------------------------------------------------
902
903 async fn add_comment(&self, a: AddCommentArgs) -> Result<Outcome<Comment>> {
Work service in Rust, with RFC 3339 timestamps904 if !a.actor.verified {
905 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
906 }
Issues and pull requests replace intents and attempts907 let body = a.body.trim();
Acceptance checks in sandboxes, line comments and review verdicts908 // An approval speaks for itself; anything else has to say something.
909 if body.is_empty() && a.verdict != Some(Verdict::Approve) {
Issues and pull requests replace intents and attempts910 return Ok(Outcome::fail(
911 FailureCode::Invalid,
912 "A comment cannot be empty.",
913 ));
914 }
Acceptance checks in sandboxes, line comments and review verdicts915 let path = a
916 .path
917 .as_deref()
918 .map(str::trim)
919 .filter(|path| !path.is_empty());
920 let line = a.line.filter(|line| *line > 0 && path.is_some());
Issues and pull requests replace intents and attempts921 if body.chars().count() > MAX_ENTRY_CHARS {
922 return Ok(Outcome::fail(
923 FailureCode::Invalid,
924 "That comment is too long.",
925 ));
926 }
927 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 look928 check!(writable(&repo));
Issues and pull requests replace intents and attempts929 // The number names an issue or a pull request, never both.
Agents as a team: lifecycle, merge queue, billing and a new shell930 let mut pull_id = None;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar931 // A command to g1t on a dependency update it opened (`@g1t rebase`)
932 // is the security service's to act on, not a mention for an agent.
933 let mut update_command = false;
Issues and pull requests replace intents and attempts934 let table = if self.issue(&repo.id, a.number).await?.is_some() {
Acceptance checks in sandboxes, line comments and review verdicts935 if path.is_some() || a.verdict.is_some() {
936 return Ok(Outcome::fail(
937 FailureCode::Invalid,
938 "Only a pull request can be reviewed or commented on by line.",
939 ));
940 }
Issues and pull requests replace intents and attempts941 "issues"
Acceptance checks in sandboxes, line comments and review verdicts942 } 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 rights943 if a.verdict.is_some() && pull.is_owned_by(&a.actor.id) {
Acceptance checks in sandboxes, line comments and review verdicts944 return Ok(Outcome::fail(
945 FailureCode::Forbidden,
946 "You cannot approve or request changes on your own pull request.",
947 ));
948 }
Agents as a team: lifecycle, merge queue, billing and a new shell949 pull_id = Some(pull.id.clone());
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar950 update_command = pull.author.is_system()
951 && g1t_contracts::updates::update_command(body).is_some();
Issues and pull requests replace intents and attempts952 "pulls"
953 } else {
Work service in Rust, with RFC 3339 timestamps954 return Ok(Outcome::fail(
Issues and pull requests replace intents and attempts955 FailureCode::NotFound,
956 "No issue or pull request has that number.",
Work service in Rust, with RFC 3339 timestamps957 ));
Issues and pull requests replace intents and attempts958 };
959
960 let now = now_ms();
961 let comment = Comment {
Agents as a team: lifecycle, merge queue, billing and a new shell962 kind: CommentKind::Comment,
Issues and pull requests replace intents and attempts963 id: new_id("cmt", now),
964 author: a.actor.clone(),
965 body: body.to_owned(),
Acceptance checks in sandboxes, line comments and review verdicts966 path: path.map(str::to_owned),
967 line,
968 verdict: a.verdict,
Issues and pull requests replace intents and attempts969 created_at: rfc3339(now),
970 };
971 self.db
972 .batch(vec![
973 self.db
974 .prepare(
975 "INSERT INTO comments
Acceptance checks in sandboxes, line comments and review verdicts976 (id, repo_id, number, author_id, author_name, body, path, line,
977 verdict, created_at)
978 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Issues and pull requests replace intents and attempts979 )
980 .bind(&[
981 comment.id.as_str().into(),
982 repo.id.as_str().into(),
983 a.number.into(),
984 a.actor.id.as_str().into(),
985 a.actor.username.as_str().into(),
986 body.into(),
Acceptance checks in sandboxes, line comments and review verdicts987 optional(&comment.path),
988 optional_number(line),
989 a.verdict
990 .map_or(JsValue::NULL, |verdict| verdict.as_str().into()),
Issues and pull requests replace intents and attempts991 comment.created_at.as_str().into(),
992 ])?,
993 self.db
994 .prepare(format!(
995 "UPDATE {table} SET updated_at = ? WHERE repo_id = ? AND number = ?"
996 ))
997 .bind(&[
998 comment.created_at.as_str().into(),
999 repo.id.as_str().into(),
1000 a.number.into(),
1001 ])?,
1002 ])
1003 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1004 if !update_command {
1005 self.note_mention(&a.actor, &repo, a.number, &comment, pull_id.as_deref()).await?;
1006 }
Issues and pull requests replace intents and attempts1007 self.publish(
1008 "comment.created",
1009 &repo.id,
1010 &a.actor,
1011 CommentCreated {
1012 comment_id: comment.id.clone(),
1013 repo_id: repo.id.clone(),
1014 number: a.number,
Agents as a team: lifecycle, merge queue, billing and a new shell1015 pull_id,
1016 verdict: a.verdict,
Issues and pull requests replace intents and attempts1017 },
1018 )
1019 .await?;
1020 Ok(Outcome::Ok(comment))
1021 }
1022
1023 // --- Pull requests -----------------------------------------------------
1024
1025 async fn open_pull(&self, a: OpenPullArgs) -> Result<Outcome<Pull>> {
1026 if !a.actor.verified {
1027 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
Work service in Rust, with RFC 3339 timestamps1028 }
Issues and pull requests replace intents and attempts1029 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 look1030 check!(writable(&repo));
1031 // g1t's own agent at work spends the workspace's compute; a pull
1032 // request anyone else's agent makes is like any other.
1033 if matches!(a.runtime, Runtime::Hosted) {
1034 check!(allowed(Some(&a.actor), &repo, Capability::Run));
1035 }
Issues and pull requests replace intents and attempts1036 let issue = match a.issue {
1037 Some(number) => match self.issue(&repo.id, number).await? {
1038 Some(issue) if issue.state == State::Open => Some(issue),
1039 Some(_) => {
1040 return Ok(Outcome::fail(
1041 FailureCode::Conflict,
1042 "This issue is closed.",
1043 ));
1044 }
1045 None => return Ok(no_issue()),
1046 },
1047 None => None,
1048 };
1049 // A pull request for an issue takes the issue's title unless given one.
1050 let title = match (a.title.trim(), &issue) {
1051 ("", Some(issue)) => issue.title.clone(),
1052 (title, _) => match valid_title(title) {
1053 Ok(title) => title.to_owned(),
1054 Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)),
1055 },
1056 };
Work service in Rust, with RFC 3339 timestamps1057 let agent = match a.agent.trim() {
1058 "" => "agent",
1059 agent => agent,
1060 };
1061 let runtime = match a.runtime {
Issues and pull requests replace intents and attempts1062 Runtime::Hosted => "hosted",
1063 Runtime::External => "external",
Work service in Rust, with RFC 3339 timestamps1064 };
1065
1066 let now = now_ms();
Issues and pull requests replace intents and attempts1067 let id = new_id("pr", now);
Pull requests from branches1068 let branch = a
1069 .branch
1070 .as_deref()
1071 .map(str::trim)
1072 .filter(|branch| !branch.is_empty());
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1073 // The branch it merges into: the default branch unless another is
1074 // asked for, which has to exist.
1075 let base = a.base.as_deref().and_then(|base| stored_base(base, &repo));
1076 if let Some(base) = &base {
1077 if branch == Some(base.as_str()) {
1078 return Ok(Outcome::fail(
1079 FailureCode::Invalid,
1080 format!("A pull request cannot merge {base} into itself. Choose another base."),
1081 ));
1082 }
1083 let exists: Option<String> = g1t_kit::call(
1084 &self.repos,
1085 "head",
1086 &HeadArgs { repo_id: repo.id.clone(), branch: base.clone() },
1087 )
1088 .await?;
1089 if exists.is_none() {
1090 return Ok(Outcome::fail(FailureCode::NotFound, format!("There is no branch named {base} to merge into.")));
1091 }
1092 }
1093 let base_name = base.clone().unwrap_or_else(|| repo.default_branch.clone());
Pull requests from branches1094 // The change is on a branch already pushed to the repository, or
1095 // will be made in a fork created for this pull request.
1096 let (fork, head) = match branch {
1097 Some(branch) => {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1098 if branch == base_name {
Pull requests from branches1099 return Ok(Outcome::fail(
1100 FailureCode::Invalid,
1101 format!("Choose a branch other than {branch}."),
1102 ));
1103 }
1104 let head: Option<String> = g1t_kit::call(
1105 &self.repos,
1106 "head",
1107 &HeadArgs {
1108 repo_id: repo.id.clone(),
1109 branch: branch.to_owned(),
1110 },
1111 )
1112 .await?;
1113 let Some(head) = head else {
1114 return Ok(Outcome::fail(
1115 FailureCode::NotFound,
1116 format!("There is no branch named {branch}. Push it first."),
1117 ));
1118 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1119 // One open pull request for each branch and base.
Pull requests from branches1120 let existing = self
1121 .db
1122 .prepare(
1123 "SELECT number AS n FROM pulls
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1124 WHERE repo_id = ? AND source_branch = ? AND status IN ('draft', 'open')
1125 AND base_branch IS ?",
Pull requests from branches1126 )
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1127 .bind(&[repo.id.as_str().into(), branch.into(), optional(&base)])?
Pull requests from branches1128 .first::<NumberRow>(None)
1129 .await?;
1130 if let Some(existing) = existing {
1131 return Ok(Outcome::fail(
1132 FailureCode::Conflict,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1133 format!("Pull request #{} is already open from {branch} into {base_name}.", existing.n),
Pull requests from branches1134 ));
1135 }
1136 (None, Some(head))
1137 }
1138 None => {
1139 let fork: Outcome<Repo> = g1t_kit::call(
1140 &self.repos,
1141 "fork_for_pull",
1142 &ForkArgs {
1143 source_id: repo.id.clone(),
1144 pull_id: id.clone(),
1145 actor: a.actor.clone(),
1146 },
1147 )
1148 .await?;
1149 (Some(check!(fork)), None)
1150 }
1151 };
1152 // A branch already holds the work, so its pull request is ready for
1153 // review from the start; one with a fork starts as a draft.
1154 let status = if branch.is_some() { "open" } else { "draft" };
1155 let body = Some(a.body.trim().to_owned()).filter(|body| !body.is_empty());
Work service in Rust, with RFC 3339 timestamps1156
Issues and pull requests replace intents and attempts1157 let number = self.next_number(&repo.id).await?;
Work service in Rust, with RFC 3339 timestamps1158 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 rights1159 // A change g1t makes is g1t's, for whoever asked for it. This is
1160 // what lifecycle::made_by_g1t reads back.
1161 let by_g1t = matches!(a.runtime, Runtime::Hosted) && agent == reviews::AGENT_NAME && fork.is_some();
1162 let (author, requested_by) = authorship(&a.actor, by_g1t);
Work service in Rust, with RFC 3339 timestamps1163 self.db
1164 .prepare(
Issues and pull requests replace intents and attempts1165 "INSERT INTO pulls
Pull requests from branches1166 (id, repo_id, number, issue_id, issue_number, title, body, agent, runtime,
1167 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 bar1168 author_id, author_name, requested_by_id, requested_by_name, created_at, updated_at,
1169 base_branch)
1170 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
Work service in Rust, with RFC 3339 timestamps1171 )
1172 .bind(&[
1173 id.as_str().into(),
Issues and pull requests replace intents and attempts1174 repo.id.as_str().into(),
1175 number.into(),
1176 optional(&issue.as_ref().map(|issue| issue.id.clone())),
1177 optional_number(issue.as_ref().map(|issue| issue.number)),
1178 title.into(),
Pull requests from branches1179 optional(&body),
Work service in Rust, with RFC 3339 timestamps1180 agent.into(),
1181 runtime.into(),
Pull requests from branches1182 status.into(),
1183 optional(&fork.as_ref().map(|fork| fork.id.clone())),
1184 optional(&fork.as_ref().map(|fork| fork.namespace.clone())),
1185 optional(&fork.as_ref().map(|fork| fork.name.clone())),
1186 optional(&branch.map(str::to_owned)),
1187 optional(&head),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1188 author.id.as_str().into(),
1189 author.username.as_str().into(),
1190 optional(&requested_by.as_ref().map(|user| user.id.clone())),
1191 optional(&requested_by.as_ref().map(|user| user.username.clone())),
Work service in Rust, with RFC 3339 timestamps1192 timestamp.as_str().into(),
1193 timestamp.as_str().into(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1194 optional(&base),
Work service in Rust, with RFC 3339 timestamps1195 ])?
1196 .run()
1197 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1198 let Some(mut pull) = self.pull(&repo.id, number).await? else {
Issues and pull requests replace intents and attempts1199 return Ok(no_pull());
Work service in Rust, with RFC 3339 timestamps1200 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1201 fill_base(&mut pull, &repo);
Agents as a team: lifecycle, merge queue, billing and a new shell1202 self.manage(&pull).await?;
1203 // Someone is on it now, so it is no longer waiting for an agent.
1204 if let Some(issue) = pull.issue {
1205 self.db
1206 .prepare("UPDATE issues SET queued_by = NULL WHERE repo_id = ? AND number = ?")
1207 .bind(&[repo.id.as_str().into(), issue.into()])?
1208 .run()
1209 .await?;
1210 }
1211 if let Some(issue) = pull.issue {
1212 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-agent1213 format!("assigned this to g1t, which opened #{}", pull.number)
Agents as a team: lifecycle, merge queue, billing and a new shell1214 } else {
1215 format!("opened #{} for this", pull.number)
1216 };
1217 self.note(&repo.id, issue, (&a.actor.id, &a.actor.username), &text)
1218 .await?;
1219 }
Issues and pull requests replace intents and attempts1220 self.publish(
1221 "pull.opened",
1222 &repo.id,
1223 &a.actor,
1224 PullEvent {
1225 agent: Some(pull.agent.clone()),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1226 base: pull.base.clone(),
Issues and pull requests replace intents and attempts1227 ..Self::pull_event(&pull)
Work service in Rust, with RFC 3339 timestamps1228 },
Issues and pull requests replace intents and attempts1229 )
Work service in Rust, with RFC 3339 timestamps1230 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1231 // Its code owners asked to review (codeowners.rs).
1232 self.refresh_code_owners(&pull).await;
1233 let pull = self.pull(&repo.id, number).await?.unwrap_or(pull);
Issues and pull requests replace intents and attempts1234 Ok(Outcome::Ok(pull))
Work service in Rust, with RFC 3339 timestamps1235 }
1236
Issues and pull requests replace intents and attempts1237 async fn list_pulls(&self, a: ListPullsArgs) -> Result<Outcome<Vec<Pull>>> {
1238 let filter = match a.state {
1239 Some(State::Open) => "AND status IN ('draft', 'open')",
1240 Some(State::Closed) => "AND status IN ('merged', 'closed')",
1241 None => "",
Work service in Rust, with RFC 3339 timestamps1242 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1243 let label = a
1244 .label
1245 .map(|label| label.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase())
1246 .filter(|label| !label.is_empty());
1247 let milestone = a.milestone;
1248 let read = |repo_id: String| {
1249 let label = label.clone();
1250 async move {
1251 let query = self
1252 .db
1253 .prepare(format!(
1254 "SELECT {PULL_COLUMNS} FROM pulls WHERE repo_id = ? {filter}
1255 AND (? IS NULL OR EXISTS
1256 (SELECT 1 FROM json_each(pulls.labels) WHERE json_each.value = ?))
1257 AND (? IS NULL OR milestone = ?)
1258 ORDER BY number DESC LIMIT ?"
1259 ))
1260 .bind(&[
1261 repo_id.into(),
1262 optional(&label),
1263 optional(&label),
1264 optional_number(milestone),
1265 optional_number(milestone),
1266 LIST_PAGE.into(),
1267 ])?;
1268 self.timing.db(1, query.all()).await?.results::<PullRow>()
1269 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1270 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1271 let (repo, rows) = check!(self.repo_then(&a.repo, &a.viewer, read).await?);
1272 let base = a.base.map(|base| base.trim().to_owned()).filter(|base| !base.is_empty());
1273 Ok(Outcome::Ok(
1274 rows.into_iter()
1275 .map(|row| {
1276 let mut pull = Pull::from(row);
1277 fill_base(&mut pull, &repo);
1278 pull
1279 })
1280 .filter(|pull| base.as_deref().is_none_or(|base| pull.base.as_deref() == Some(base)))
1281 .collect(),
1282 ))
Issues and pull requests replace intents and attempts1283 }
1284
Fast pages, required checks on the branch, self-hosted runners, honest incidents1285 /// `pulls_for_repos`: what `list_pulls` gives, open and closed, for many
1286 /// repositories at once: one access check with repos for all of them
1287 /// and one query, instead of two of each per repository.
1288 async fn pulls_for_repos(&self, a: PullsForReposArgs) -> Result<Vec<RepoPulls>> {
1289 let ids: Vec<String> = a.repo_ids.into_iter().take(MAX_PULLS_FOR_REPOS).collect();
1290 if ids.is_empty() {
1291 return Ok(Vec::new());
1292 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1293 let limit = a.limit.clamp(1, LIST_PAGE);
1294 // The rows are read beside the access check, for every id asked
1295 // about; those of repositories the viewer cannot read are dropped.
1296 let asked = serde_json::to_string(&ids)?;
1297 let check = ReadableArgs { ids, viewer: a.viewer };
1298 let readable = self.timing.rpc(g1t_kit::call::<_, Vec<Repo>>(&self.repos, "readable", &check));
1299 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 incidents1300 if readable.is_empty() {
1301 return Ok(Vec::new());
1302 }
1303 let mut answer: Vec<RepoPulls> = readable
1304 .iter()
1305 .map(|repo| RepoPulls { repo_id: repo.id.clone(), open: Vec::new(), closed: Vec::new() })
1306 .collect();
1307 for pull in rows.into_iter().map(Pull::from) {
1308 let Some(entry) = answer.iter_mut().find(|entry| entry.repo_id == pull.repo_id) else {
1309 continue;
1310 };
1311 match pull.status {
1312 PullStatus::Draft | PullStatus::Open => entry.open.push(pull),
1313 PullStatus::Merged | PullStatus::Closed => entry.closed.push(pull),
1314 }
1315 }
1316 for entry in &mut answer {
1317 entry.open.sort_by_key(|pull| std::cmp::Reverse(pull.number));
1318 entry.closed.sort_by_key(|pull| std::cmp::Reverse(pull.number));
1319 }
1320 Ok(answer)
1321 }
1322
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1323 /// The newest `limit` of each repository's open (draft or open) and
1324 /// closed (merged or closed) pull requests, for the ids in `ids` (JSON).
1325 async fn newest_pulls(&self, ids: String, limit: u32) -> Result<Vec<PullRow>> {
1326 self.db
1327 .prepare(format!(
1328 "SELECT * FROM (
1329 SELECT {PULL_COLUMNS}, ROW_NUMBER() OVER (
1330 PARTITION BY pulls.repo_id, pulls.status IN ('draft', 'open') ORDER BY pulls.number DESC
1331 ) AS place
1332 FROM pulls WHERE pulls.repo_id IN (SELECT value FROM json_each(?1))
1333 ) WHERE place <= ?2"
1334 ))
1335 .bind(&[ids.into(), limit.into()])?
1336 .all()
1337 .await?
1338 .results::<PullRow>()
1339 }
1340
Issues and pull requests replace intents and attempts1341 async fn get_pull(&self, a: ViewArgs) -> Result<Outcome<PullDetail>> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1342 let number = a.number;
1343 // Every row the page and the lifecycle read, in one batch started
1344 // beside the access check; the helpers below read from it.
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1345 let namespace = a.repo.namespace.clone();
1346 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 daily1347 let Outcome::Ok((repo, Some(found))) = self.repo_then(&a.repo, &a.viewer, read).await? else {
1348 return Ok(no_pull());
1349 };
1350 let Some(row) = found.first::<PullRow>(prefetch::Slot::Pull)? else {
1351 return Ok(no_pull());
Work service in Rust, with RFC 3339 timestamps1352 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1353 let stored = found.first::<StoredBehind>(prefetch::Slot::Pull)?;
1354 let issue = found.first::<IssueRow>(prefetch::Slot::Issue)?.map(Issue::from);
1355 let comments: Vec<Comment> =
1356 found.rows::<CommentRow>(prefetch::Slot::Comments)?.into_iter().map(Comment::from).collect();
1357 self.keep_prefetched(Some(found));
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1358 let default_branch = repo.default_branch.clone();
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1359 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 daily1360 self.keep_prefetched(None);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1361 // The branch it merges into, named, for whoever reads it.
1362 Ok(match detail? {
1363 Outcome::Ok(mut detail) => {
1364 if detail.pull.base.as_deref().is_none_or(str::is_empty) {
1365 detail.pull.base = Some(default_branch);
1366 }
1367 Outcome::Ok(detail)
1368 }
1369 failed => failed,
1370 })
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1371 }
1372
1373 async fn pull_detail(
1374 &self,
1375 repo: Repo,
1376 mut pull: Pull,
1377 issue: Option<Issue>,
1378 comments: Vec<Comment>,
1379 stored: Option<StoredBehind>,
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1380 viewer: &Viewer,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1381 ) -> Result<Outcome<PullDetail>> {
1382 // Whether it is behind, as worked out with its mergeability on the
1383 // last push to either side (mergeability.rs), when that was for
1384 // its head as it is now; otherwise asked of the repos service.
1385 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 shell1386 // Worked out on each push; this covers a pull request from before
1387 // that was recorded.
1388 if pull.files.is_empty() && pull.head_commit.is_some() {
1389 pull.files = self.refresh_files(&pull).await?;
1390 }
1391 // Everything else at once: none of it depends on the rest, and each
1392 // is a round trip of its own.
1393 let standing = async {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1394 // Mergeability first: where g1t sees a pull request through, a
1395 // conflict decides its next step.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1396 let behind = async {
1397 match known_behind {
1398 Some(behind) => Ok(behind),
1399 None => {
1400 let behind = self.is_behind(&repo.id, &pull).await?;
1401 // Kept for the next view when the mergeability on
1402 // record is for this head: a pull request from
1403 // before `behind` was kept asks once.
1404 if let Some(head) = pull.head_commit.as_deref()
1405 && pull.status.is_active()
1406 {
1407 self.db
1408 .prepare(
1409 "UPDATE pulls SET behind = ?1
1410 WHERE id = ?2 AND behind IS NULL AND mergeable_key LIKE ?3 || '..%'",
1411 )
1412 .bind(&[u32::from(behind).into(), pull.id.as_str().into(), head.into()])?
1413 .run()
1414 .await?;
1415 }
1416 Ok(behind)
1417 }
1418 }
1419 };
1420 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 step1421 let assessed = self.assess_with_confidence(&pull, &issue, behind).await?;
1422 let confidence = assessed.as_ref().and_then(|(_, _, confidence)| confidence.clone());
1423 let lifecycle = assessed.map(|(lifecycle, _, _)| lifecycle);
1424 Ok::<_, worker::Error>((behind, (lifecycle, confidence), merge))
Agents as a team: lifecycle, merge queue, billing and a new shell1425 };
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1426 let (((behind, (lifecycle, confidence), (mergeable, conflicts)), (landing, stalled), comments), (checks, overlaps, review_pending)) =
Agents as a team: lifecycle, merge queue, billing and a new shell1427 try_join(
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1428 try_join3(standing, self.landing_state(&pull.id), async { Ok(comments) }),
Agents as a team: lifecycle, merge queue, billing and a new shell1429 try_join3(
1430 self.latest_checks(&pull.id),
1431 self.overlaps(&pull),
1432 self.review_pending(&pull.id),
1433 ),
1434 )
1435 .await?;
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1436 // As just worked out, rather than as it was read.
1437 if confidence.is_some() {
1438 pull.confidence = confidence;
1439 }
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1440 // The rules of the branch it merges into, as they stack, and which
1441 // of them it does not meet yet, for whoever is looking.
1442 let (statuses, settings, gate) = try_join3(
1443 self.statuses(&repo.id, pull.head_commit.as_deref()),
1444 self.settings_on(&repo, &pull),
1445 async {
1446 if pull.status.is_active() {
1447 self.merge_gate(&repo, &pull, viewer.as_ref(), false, true).await.map(Some)
1448 } else {
1449 Ok(None)
1450 }
1451 },
1452 )
1453 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1454 let code_owners = self.pull_code_owners(&pull, &comments, &settings).await?;
Issues and pull requests replace intents and attempts1455 Ok(Outcome::Ok(PullDetail {
Fast pages, required checks on the branch, self-hosted runners, honest incidents1456 required_checks: required_checks(&settings.required_checks, &statuses),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1457 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 bar1458 code_owners,
Agents as a team: lifecycle, merge queue, billing and a new shell1459 comments,
1460 checks,
1461 overlaps,
1462 behind,
1463 review_pending,
1464 lifecycle,
1465 landing,
1466 stalled,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request1467 messages: self.messages(&pull.id).await?,
Fast pages, required checks on the branch, self-hosted runners, honest incidents1468 statuses,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1469 mergeable,
1470 conflicts,
1471 earlier_checks: self.earlier_checks(&pull.id).await?,
Issues and pull requests replace intents and attempts1472 issue,
1473 pull,
1474 }))
Work service in Rust, with RFC 3339 timestamps1475 }
1476
Issues and pull requests replace intents and attempts1477 /// The pull request, if it is still active and `actor` opened it or
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1478 /// may triage the repository's pull requests.
Issues and pull requests replace intents and attempts1479 async fn manageable_pull(
Work service in Rust, with RFC 3339 timestamps1480 &self,
Issues and pull requests replace intents and attempts1481 actor: &User,
1482 path: &RepoPath,
1483 number: u32,
1484 ) -> Result<Outcome<Pull>> {
1485 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 look1486 check!(writable(&repo));
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1487 if !pull.is_owned_by(&actor.id) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1488 check!(allowed(Some(actor), &repo, Capability::Triage));
Issues and pull requests replace intents and attempts1489 }
1490 if !pull.status.is_active() {
1491 return Ok(Outcome::fail(
Work service in Rust, with RFC 3339 timestamps1492 FailureCode::Conflict,
Issues and pull requests replace intents and attempts1493 format!("This pull request is already {}.", pull.status.as_str()),
Work service in Rust, with RFC 3339 timestamps1494 ));
1495 }
Issues and pull requests replace intents and attempts1496 Ok(Outcome::Ok(pull))
1497 }
1498
Catching up with main takes seconds when the two sides touched different files1499 /// Brings a pull request up to date with the default branch without a
1500 /// 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 rights1501 /// have pushed the merge themselves may ask: whoever opened it (or asked
1502 /// g1t for it), for a fork; anyone who may push, for a branch of the
1503 /// repository. When it needs a
Catching up with main takes seconds when the two sides touched different files1504 /// real merge, says so, naming the conflicting files if a probe found
1505 /// them, and pushes nothing.
1506 async fn catch_up_pull(&self, a: PullActionArgs) -> Result<Outcome<PullBranchUpdate>> {
1507 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 look1508 check!(writable(&repo));
Catching up with main takes seconds when the two sides touched different files1509 if !pull.status.is_active() {
1510 return Ok(Outcome::fail(
1511 FailureCode::Conflict,
1512 format!("This pull request is already {}.", pull.status.as_str()),
1513 ));
1514 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1515 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 rights1516 if !pull.is_owned_by(&a.actor.id) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1517 return Ok(Outcome::fail(
1518 FailureCode::Forbidden,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1519 "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 look1520 ));
1521 }
Catching up with main takes seconds when the two sides touched different files1522 } else {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1523 check!(allowed(Some(&a.actor), &repo, Capability::Push));
Catching up with main takes seconds when the two sides touched different files1524 }
1525 let updated: Outcome<PullBranchUpdate> = g1t_kit::call(
1526 &self.repos,
1527 "update_pull_branch",
1528 &UpdatePullBranchArgs {
1529 source_id: pull.fork_repo_id.clone().unwrap_or_else(|| repo.id.clone()),
1530 branch: pull.branch.clone(),
1531 number: pull.number,
1532 actor: a.actor,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1533 target_branch: pull.base.clone(),
Catching up with main takes seconds when the two sides touched different files1534 },
1535 )
1536 .await?;
1537 // A probe that found conflicts says more than "both changed it".
1538 if let Outcome::Ok(PullBranchUpdate::NeedsAgent { .. }) = &updated
1539 && let Some(files) = self.conflicting_files(&pull).await?
1540 && !files.is_empty()
1541 {
1542 return Ok(Outcome::Ok(PullBranchUpdate::NeedsAgent {
1543 reason: NeedsAgentReason::Conflicting,
1544 detail: "Merging it conflicts.".to_owned(),
1545 paths: files,
1546 }));
1547 }
1548 Ok(updated)
1549 }
1550
Agents as a team: lifecycle, merge queue, billing and a new shell1551 async fn update_pull(&self, a: UpdatePullArgs) -> Result<Outcome<Pull>> {
1552 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 bar1553 // The branch it merges into, its milestone and its labels first:
1554 // each can be refused, and then nothing else changes.
1555 if a.base.is_some() || a.milestone.is_some() || a.labels.is_some() {
1556 let repo = check!(self.repo(&a.repo, &Some(a.actor.clone())).await?);
1557 if let Some(base) = &a.base {
1558 check!(self.change_base(&a.actor, &repo, &pull, base).await?);
1559 }
1560 if let Some(number) = a.milestone {
1561 check!(self.set_milestone(&a.actor, &repo, &labels::Item::Pull(pull.clone()), number).await?);
1562 }
1563 if let Some(labels) = &a.labels {
1564 check!(self.relabel(&a.actor, &repo, &labels::Item::Pull(pull.clone()), labels).await?);
1565 }
1566 }
Agents as a team: lifecycle, merge queue, billing and a new shell1567 let assignees = match a.assignees {
1568 Some(names) => Some(check!(self.valid_assignees(names).await?)),
1569 None => None,
1570 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1571 // Teams, named `workspace/team`, apart from the people.
1572 let (team_names, a_reviewers) = match a.reviewers {
1573 Some(names) => {
1574 let (teams, people): (Vec<String>, Vec<String>) =
1575 names.into_iter().partition(|name| team_reviews::team_name(name).is_some());
1576 (Some(teams), Some(people))
1577 }
1578 None => (None, None),
1579 };
1580 let teams = match team_names {
1581 Some(names) => Some(check!(self.valid_team_reviewers(&a.actor, &a.repo, &pull, names).await?)),
1582 None => None,
1583 };
1584 let reviewers = match a_reviewers {
Agents as a team: lifecycle, merge queue, billing and a new shell1585 Some(names) => {
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1586 // g1t is not an account; everyone else has to be.
Agents as a team: lifecycle, merge queue, billing and a new shell1587 let agent = names
1588 .iter()
1589 .any(|name| name.trim().eq_ignore_ascii_case(reviews::AGENT_NAME));
1590 let people = names
1591 .into_iter()
1592 .filter(|name| !name.trim().eq_ignore_ascii_case(reviews::AGENT_NAME))
1593 .collect();
1594 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 rights1595 // Nobody is asked to review their own, nor what they had g1t make.
1596 reviewers.retain(|name| *name != pull.owner().username);
Agents as a team: lifecycle, merge queue, billing and a new shell1597 if agent {
1598 reviewers.insert(0, reviews::AGENT_NAME.to_owned());
1599 }
1600 Some(reviewers)
1601 }
1602 None => None,
1603 };
1604 self.db
1605 .prepare(
1606 "UPDATE pulls
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1607 SET assignees = COALESCE(?, assignees), updated_at = ?
Agents as a team: lifecycle, merge queue, billing and a new shell1608 WHERE id = ?",
1609 )
1610 .bind(&[
1611 optional(&assignees.as_ref().map(serde_json::to_string).transpose()?),
1612 rfc3339(now_ms()).into(),
1613 pull.id.as_str().into(),
1614 ])?
1615 .run()
1616 .await?;
1617 if let Some(assignees) = &assignees {
1618 self.note_changes(
1619 &pull.repo_id,
1620 pull.number,
1621 &a.actor,
1622 &pull.assignees,
1623 assignees,
1624 ("assigned", "unassigned"),
1625 )
1626 .await?;
1627 }
Events: review requests, assignments, stops and deployments are published1628 // Who was newly assigned or asked to review, and whose request was
1629 // withdrawn: the inbox tells them, and webhooks say so.
1630 let newly = |after: &[String], before: &[String]| -> Vec<String> {
1631 after.iter().filter(|name| !before.contains(name)).cloned().collect()
1632 };
1633 if let Some(assignees) = &assignees {
1634 let added = newly(assignees, &pull.assignees);
1635 if !added.is_empty() {
1636 self.publish(
1637 "pull.assigned",
1638 &pull.repo_id,
1639 &a.actor,
1640 PullEvent {
1641 assignees: Some(assignees.clone()),
1642 added: Some(added),
1643 ..Self::pull_event(&pull)
1644 },
1645 )
1646 .await?;
1647 }
1648 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1649 if reviewers.is_some() || teams.is_some() {
1650 let people = reviewers.unwrap_or_else(|| pull.reviewers.clone());
1651 let teams = teams.unwrap_or_else(|| pull.team_reviewers.clone());
1652 self.set_reviewers(&pull, people, teams, Some(&a.actor), false).await?;
Events: review requests, assignments, stops and deployments are published1653 }
Agents as a team: lifecycle, merge queue, billing and a new shell1654 Ok(match self.pull(&pull.repo_id, pull.number).await? {
1655 Some(pull) => Outcome::Ok(pull),
1656 None => no_pull(),
1657 })
1658 }
1659
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1660 /// Points an open pull request at another branch to merge into. Needs
1661 /// the Write role. What it would merge, whether it is behind, its
1662 /// mergeability and its checks are all worked out against the new base.
1663 async fn change_base(&self, actor: &User, repo: &Repo, pull: &Pull, base: &str) -> Result<Outcome<()>> {
1664 check!(allowed(Some(actor), repo, Capability::Push));
1665 let base = base.trim();
1666 if base.is_empty() {
1667 return Ok(Outcome::fail(FailureCode::Invalid, "Name the branch it should merge into."));
1668 }
1669 let before = pull.base_branch(&repo.default_branch).to_owned();
1670 if base == before {
1671 return Ok(Outcome::Ok(()));
1672 }
1673 if pull.fork_repo_id.is_none() && pull.branch.as_deref() == Some(base) {
1674 return Ok(Outcome::fail(
1675 FailureCode::Invalid,
1676 format!("A pull request cannot merge {base} into itself. Choose another base."),
1677 ));
1678 }
1679 let exists: Option<String> = g1t_kit::call(
1680 &self.repos,
1681 "head",
1682 &HeadArgs { repo_id: repo.id.clone(), branch: base.to_owned() },
1683 )
1684 .await?;
1685 if exists.is_none() {
1686 return Ok(Outcome::fail(FailureCode::NotFound, format!("There is no branch named {base} to merge into.")));
1687 }
1688 // In the merge queue it was headed for the default branch; it
1689 // leaves the queue for another base.
1690 let left_queue = self
1691 .leave(&pull.repo_id, pull, QueueState::Removed, Some("Its base branch changed."))
1692 .await?;
1693 let stored = stored_base(base, repo);
1694 self.db
1695 .prepare(
1696 "UPDATE pulls
1697 SET base_branch = ?, updated_at = ?, land_requested = NULL, land_requested_at = NULL, behind = NULL,
1698 mergeable = NULL, mergeable_key = NULL, conflicts = NULL
1699 WHERE id = ?",
1700 )
1701 .bind(&[optional(&stored), rfc3339(now_ms()).into(), pull.id.as_str().into()])?
1702 .run()
1703 .await?;
1704 self.note(
1705 &pull.repo_id,
1706 pull.number,
1707 (&actor.id, &actor.username),
1708 &format!("changed the base branch from `{before}` to `{base}`"),
1709 )
1710 .await?;
1711 self.publish(
1712 "pull.base_changed",
1713 &pull.repo_id,
1714 actor,
1715 PullEvent { base: Some(base.to_owned()), ..Self::pull_event(pull) },
1716 )
1717 .await?;
1718 if left_queue {
1719 self.publish_as(
1720 "queue.changed",
1721 &pull.repo_id,
1722 None,
1723 g1t_contracts::events::QueueChanged { repo_id: pull.repo_id.clone() },
1724 )
1725 .await?;
1726 }
1727 // Whether it merges cleanly into the new base.
1728 if let Some(moved) = self.pull_by_id(&pull.id).await?
1729 && let Err(error) = self.assess_mergeability(&moved).await
1730 {
1731 worker::console_warn!("mergeability of {}: {error}", pull.id);
1732 }
1733 Ok(Outcome::Ok(()))
1734 }
1735
Issues and pull requests replace intents and attempts1736 /// Marks a draft ready for review, or updates the description of one
1737 /// that already is.
1738 async fn ready_pull(&self, a: PullActionArgs) -> Result<Outcome<Pull>> {
1739 let mut pull = check!(self.manageable_pull(&a.actor, &a.repo, a.number).await?);
Work service in Rust, with RFC 3339 timestamps1740 let summary = Some(a.summary.trim().to_owned()).filter(|summary| !summary.is_empty());
1741 let now = rfc3339(now_ms());
1742 self.db
1743 .prepare(
Issues and pull requests replace intents and attempts1744 "UPDATE pulls SET status = 'open', body = COALESCE(?, body), updated_at = ?
Work service in Rust, with RFC 3339 timestamps1745 WHERE id = ?",
1746 )
1747 .bind(&[
1748 optional(&summary),
1749 now.as_str().into(),
Issues and pull requests replace intents and attempts1750 pull.id.as_str().into(),
Work service in Rust, with RFC 3339 timestamps1751 ])?
1752 .run()
1753 .await?;
Issues and pull requests replace intents and attempts1754 if pull.status == PullStatus::Draft {
Workflows run when an agent's pull request is marked ready1755 // The head as it is now: the push that came just before may not
1756 // have reached `head_commit` yet, and workflows run on it.
1757 let commit = self.live_head(&pull).await?.or_else(|| pull.head_commit.clone());
Issues and pull requests replace intents and attempts1758 self.publish(
1759 "pull.ready",
1760 &pull.repo_id,
1761 &a.actor,
Workflows run when an agent's pull request is marked ready1762 PullEvent {
1763 commit,
1764 ..Self::pull_event(&pull)
1765 },
Issues and pull requests replace intents and attempts1766 )
1767 .await?;
1768 }
Agents as a team: lifecycle, merge queue, billing and a new shell1769 if pull.status == PullStatus::Draft {
1770 self.note(
1771 &pull.repo_id,
1772 pull.number,
1773 (&a.actor.id, &a.actor.username),
1774 "marked this ready for review",
1775 )
1776 .await?;
1777 }
Issues and pull requests replace intents and attempts1778 pull.status = PullStatus::Open;
1779 pull.body = summary.or(pull.body);
1780 pull.updated_at = now;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1781 // A draft's code owners are asked once it is ready.
1782 self.refresh_code_owners(&pull).await;
1783 if let Some(fresh) = self.pull(&pull.repo_id, pull.number).await? {
1784 pull.reviewers = fresh.reviewers;
1785 pull.team_reviewers = fresh.team_reviewers;
1786 }
Issues and pull requests replace intents and attempts1787 Ok(Outcome::Ok(pull))
1788 }
1789
1790 async fn close_pull(&self, a: PullActionArgs) -> Result<Outcome<Pull>> {
1791 let mut pull = check!(self.manageable_pull(&a.actor, &a.repo, a.number).await?);
1792 let now = rfc3339(now_ms());
1793 self.db
1794 .prepare("UPDATE pulls SET status = 'closed', updated_at = ? WHERE id = ?")
1795 .bind(&[now.as_str().into(), pull.id.as_str().into()])?
1796 .run()
1797 .await?;
1798 self.publish(
1799 "pull.closed",
1800 &pull.repo_id,
1801 &a.actor,
1802 Self::pull_event(&pull),
1803 )
Work service in Rust, with RFC 3339 timestamps1804 .await?;
Agents as a team: lifecycle, merge queue, billing and a new shell1805 self.note(
1806 &pull.repo_id,
1807 pull.number,
1808 (&a.actor.id, &a.actor.username),
1809 "closed this",
1810 )
1811 .await?;
1812 // A closed pull request leaves the merge queue.
1813 if self
1814 .leave(&pull.repo_id, &pull, QueueState::Removed, Some("It was closed."))
1815 .await?
1816 {
1817 self.publish_as(
1818 "queue.changed",
1819 &pull.repo_id,
1820 None,
1821 g1t_contracts::events::QueueChanged {
1822 repo_id: pull.repo_id.clone(),
1823 },
1824 )
1825 .await?;
1826 }
Issues and pull requests replace intents and attempts1827 pull.status = PullStatus::Closed;
1828 pull.updated_at = now;
1829 Ok(Outcome::Ok(pull))
Work service in Rust, with RFC 3339 timestamps1830 }
1831
Issues and pull requests replace intents and attempts1832 /// Lands the pull request on the repository's default branch. Unless
1833 /// told to keep it open, that resolves the issue it was for: the issue
1834 /// closes naming this pull request, and the others still in progress
1835 /// for it close as superseded.
1836 async fn merge_pull(&self, a: PullActionArgs) -> Result<Outcome<Pull>> {
Work service in Rust, with RFC 3339 timestamps1837 let viewer = Some(a.actor.clone());
Agents as a team: lifecycle, merge queue, billing and a new shell1838 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 look1839 check!(writable(&repo));
Issues and pull requests replace intents and attempts1840 match pull.status {
1841 PullStatus::Open => {}
1842 PullStatus::Draft => {
1843 return Ok(Outcome::fail(
1844 FailureCode::Conflict,
1845 "This pull request is still a draft. Mark it ready for review first.",
1846 ));
1847 }
1848 status => {
1849 return Ok(Outcome::fail(
1850 FailureCode::Conflict,
1851 format!("This pull request is already {}.", status.as_str()),
1852 ));
1853 }
Work service in Rust, with RFC 3339 timestamps1854 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1855 let base = pull.base_branch(&repo.default_branch).to_owned();
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1856 // The rules of the branch it merges into, as they stack: the one
1857 // gate for the merge button, the API, MCP, auto-merge and the queue,
1858 // for a person's pull request and an agent's alike. A bypass counts
1859 // when the merger asks for it, or for g1t when a ruleset lists it.
1860 let gate = self.merge_gate(&repo, &pull, Some(&a.actor), a.ignore_checks, true).await?;
1861 let bypassable = gate.bypassable();
1862 let gate = if a.bypass_rules || a.actor.is_system() { gate } else { gate.without_bypass() };
1863 let settings = rulesets::overlay(self.settings(&repo.id).await?, &gate.requirements, base == repo.default_branch);
1864 if pull.check_status == Some(CheckStatus::Failed) && !(a.ignore_checks && settings.allow_ignoring_checks) {
1865 return Ok(Outcome::fail(
1866 FailureCode::Conflict,
1867 "It failed in the merge queue; push a fix to try again.",
1868 ));
1869 }
1870 if let Some(refusal) = gate.refusal() {
1871 if access::can(Some(&a.actor), &repo, Capability::Merge) {
1872 self.record_merge_evaluations(&repo, &pull, &gate).await;
Acceptance checks in sandboxes, line comments and review verdicts1873 }
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1874 let offer = if bypassable && !a.bypass_rules {
1875 " You may bypass these rules: merge again and ask to bypass them (bypass_rules)."
1876 } else {
1877 ""
1878 };
1879 return Ok(Outcome::fail(FailureCode::Conflict, format!("{refusal}{offer}")));
Agents as a team: lifecycle, merge queue, billing and a new shell1880 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1881 // Known ahead of time to conflict: neither a merge nor the queue
1882 // would get through, so say what has to be resolved now.
1883 if let Some(files) = self.conflicting_files(&pull).await? {
1884 let named = if files.is_empty() {
1885 String::new()
1886 } else {
1887 format!(" in {}", files.join(", "))
1888 };
1889 return Ok(Outcome::fail(
1890 FailureCode::Conflict,
1891 format!(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1892 "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 domains1893 ),
1894 ));
1895 }
Work service in Rust, with RFC 3339 timestamps1896
Agents as a team: lifecycle, merge queue, billing and a new shell1897 // A repository that merges through a queue: it joins the queue, and
1898 // lands once its state together with everything ahead has passed.
1899 if settings.merge_queue {
1900 if !a.actor.verified {
1901 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1902 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1903 check!(allowed(Some(&a.actor), &repo, Capability::Merge));
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1904 self.record_merge_evaluations(&repo, &pull, &gate).await;
Agents as a team: lifecycle, merge queue, billing and a new shell1905 return self.enqueue(&repo, &pull, &a.actor, a.keep_issue_open).await;
1906 }
1907
1908 // The default branch has moved under it. Unless the repository
1909 // insists on that being dealt with first, bring it up to date and
1910 // land it when that is done.
1911 if self.is_behind(&repo.id, &pull).await? {
1912 if settings.require_up_to_date {
1913 return Ok(Outcome::fail(
1914 FailureCode::Conflict,
1915 format!(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1916 "{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 shell1917 ),
1918 ));
1919 }
1920 if !a.actor.verified {
1921 return Ok(Outcome::fail(FailureCode::Forbidden, UNVERIFIED));
1922 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1923 check!(allowed(Some(&a.actor), &repo, Capability::Merge));
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1924 self.record_merge_evaluations(&repo, &pull, &gate).await;
Agents as a team: lifecycle, merge queue, billing and a new shell1925 self.request_landing(&pull, &a.actor, a.keep_issue_open)
1926 .await?;
1927 return Ok(Outcome::Ok(pull));
1928 }
1929
Issues and pull requests replace intents and attempts1930 // Whether the actor may write to the repository is decided by repos.
Work service in Rust, with RFC 3339 timestamps1931 let landed: Outcome<Landed> = g1t_kit::call(
1932 &self.repos,
1933 "land",
1934 &LandArgs {
Pull requests from branches1935 // A pull request from a branch lands from the repository itself.
1936 source_id: pull.fork_repo_id.clone().unwrap_or_else(|| repo.id.clone()),
1937 branch: pull.branch.clone(),
Work service in Rust, with RFC 3339 timestamps1938 actor: a.actor.clone(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1939 target_branch: Some(base.clone()),
Work service in Rust, with RFC 3339 timestamps1940 },
1941 )
1942 .await?;
Issues and pull requests replace intents and attempts1943 let landed = check!(landed);
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1944 self.record_merge_evaluations(&repo, &pull, &gate).await;
Agents as a team: lifecycle, merge queue, billing and a new shell1945 Ok(Outcome::Ok(
1946 self.record_merge(&repo, pull, &a.actor, a.keep_issue_open, landed)
1947 .await?,
1948 ))
1949 }
Work service in Rust, with RFC 3339 timestamps1950
Agents as a team: lifecycle, merge queue, billing and a new shell1951 /// Records a pull request as merged once the default branch holds it:
1952 /// closes its issue, supersedes the others for it, and says so.
1953 pub(crate) async fn record_merge(
1954 &self,
1955 repo: &Repo,
1956 mut pull: Pull,
1957 actor: &User,
1958 keep_issue_open: bool,
1959 landed: Landed,
1960 ) -> Result<Pull> {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1961 // Only a merge into the default branch resolves the issue: into
1962 // another branch, the work has not landed yet.
1963 let keep_issue_open = keep_issue_open || !pull.targets_default(&repo.default_branch);
Agents as a team: lifecycle, merge queue, billing and a new shell1964 let issue = match pull.issue {
1965 Some(number) if !keep_issue_open => self
1966 .issue(&repo.id, number)
1967 .await?
1968 .filter(|issue| issue.state == State::Open),
1969 _ => None,
1970 };
Work service in Rust, with RFC 3339 timestamps1971 let now = rfc3339(now_ms());
Issues and pull requests replace intents and attempts1972 let mut statements = vec![
1973 self.db
1974 .prepare(
1975 "UPDATE pulls
1976 SET status = 'merged', head_commit = ?, merge_base = ?, merged_by = ?,
1977 merged_at = ?, updated_at = ?
1978 WHERE id = ?",
1979 )
1980 .bind(&[
1981 landed.commit.as_str().into(),
1982 optional(&landed.previous),
Agents as a team: lifecycle, merge queue, billing and a new shell1983 actor.username.as_str().into(),
Issues and pull requests replace intents and attempts1984 now.as_str().into(),
1985 now.as_str().into(),
1986 pull.id.as_str().into(),
1987 ])?,
1988 ];
1989 if let Some(issue) = &issue {
1990 statements.push(
Work service in Rust, with RFC 3339 timestamps1991 self.db
1992 .prepare(
Issues and pull requests replace intents and attempts1993 "UPDATE issues
1994 SET state = 'closed', reason = 'completed', resolved_by = ?,
1995 closed_at = ?, updated_at = ?
Work service in Rust, with RFC 3339 timestamps1996 WHERE id = ?",
1997 )
1998 .bind(&[
Issues and pull requests replace intents and attempts1999 pull.number.into(),
2000 now.as_str().into(),
Work service in Rust, with RFC 3339 timestamps2001 now.as_str().into(),
Issues and pull requests replace intents and attempts2002 issue.id.as_str().into(),
Work service in Rust, with RFC 3339 timestamps2003 ])?,
Issues and pull requests replace intents and attempts2004 );
2005 statements.push(
Work service in Rust, with RFC 3339 timestamps2006 self.db
Issues and pull requests replace intents and attempts2007 .prepare(
2008 "UPDATE pulls SET status = 'closed', superseded_by = ?, updated_at = ?
2009 WHERE issue_id = ? AND id != ? AND status IN ('draft', 'open')",
2010 )
2011 .bind(&[
2012 pull.number.into(),
2013 now.as_str().into(),
2014 issue.id.as_str().into(),
2015 pull.id.as_str().into(),
2016 ])?,
2017 );
2018 }
2019 self.db.batch(statements).await?;
2020
2021 self.publish(
2022 "pull.merged",
2023 &repo.id,
Agents as a team: lifecycle, merge queue, billing and a new shell2024 actor,
Issues and pull requests replace intents and attempts2025 PullEvent {
Work service in Rust, with RFC 3339 timestamps2026 commit: Some(landed.commit.clone()),
Issues and pull requests replace intents and attempts2027 ..Self::pull_event(&pull)
Work service in Rust, with RFC 3339 timestamps2028 },
Issues and pull requests replace intents and attempts2029 )
Work service in Rust, with RFC 3339 timestamps2030 .await?;
Issues and pull requests replace intents and attempts2031 if let Some(issue) = &issue {
2032 self.publish(
2033 "issue.closed",
2034 &repo.id,
Agents as a team: lifecycle, merge queue, billing and a new shell2035 actor,
Issues and pull requests replace intents and attempts2036 IssueEvent {
2037 reason: Some(IssueReason::Completed.as_str()),
2038 resolved_by: Some(pull.number),
2039 ..Self::issue_event(issue)
2040 },
2041 )
2042 .await?;
2043 }
Work service in Rust, with RFC 3339 timestamps2044
Agents as a team: lifecycle, merge queue, billing and a new shell2045 let who = (actor.id.as_str(), actor.username.as_str());
2046 self.note(&repo.id, pull.number, who, "merged this").await?;
2047 if let Some(issue) = &issue {
2048 self.note(
2049 &repo.id,
2050 issue.number,
2051 who,
2052 &format!("closed this by merging #{}", pull.number),
2053 )
2054 .await?;
2055 }
Issues and pull requests replace intents and attempts2056 pull.status = PullStatus::Merged;
Agents as a team: lifecycle, merge queue, billing and a new shell2057 pull.head_commit = Some(landed.commit.clone());
Issues and pull requests replace intents and attempts2058 pull.merge_base = landed.previous;
Agents as a team: lifecycle, merge queue, billing and a new shell2059 pull.merged_by = Some(actor.username.clone());
Issues and pull requests replace intents and attempts2060 pull.merged_at = Some(now.clone());
2061 pull.updated_at = now;
Agents as a team: lifecycle, merge queue, billing and a new shell2062 Ok(pull)
Work service in Rust, with RFC 3339 timestamps2063 }
2064
Issues and pull requests replace intents and attempts2065 async fn list_active_pulls(&self, a: ViewerArgs) -> Result<Vec<ActivePull>> {
Work service in Rust, with RFC 3339 timestamps2066 let Some(viewer) = a.viewer else {
2067 return Ok(Vec::new());
2068 };
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2069 // The pull requests and their issues, in one round trip: their own,
2070 // and those g1t made for them (Pull::owner).
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2071 let author = [JsValue::from(viewer.id.as_str())];
Agents as a team: lifecycle, merge queue, billing and a new shell2072 let found = self
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2073 .timing
2074 .db(
2075 2,
2076 self.db.batch(vec![
2077 self.db
2078 .prepare(format!(
2079 "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 rights2080 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 daily2081 ORDER BY updated_at DESC LIMIT 50"
2082 ))
2083 .bind(&author)?,
2084 self.db
2085 .prepare(format!(
2086 "SELECT {ISSUE_COLUMNS} FROM issues WHERE issues.id IN (
2087 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 rights2088 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 daily2089 ORDER BY updated_at DESC LIMIT 50)"
2090 ))
2091 .bind(&author)?,
2092 ]),
2093 )
Agents as a team: lifecycle, merge queue, billing and a new shell2094 .await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2095 let (Some(found), Some(issues)) = (found.first(), found.get(1)) else {
2096 return Ok(Vec::new());
2097 };
Agents as a team: lifecycle, merge queue, billing and a new shell2098 let snapshots = found.results::<Snapshot>()?;
2099 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 daily2100 let issues: Vec<Issue> = issues.results::<IssueRow>()?.into_iter().map(Issue::from).collect();
2101 let issues = &issues;
2102 // Where each stands: the remembered assessment when there is one,
2103 // and worked out otherwise.
Agents as a team: lifecycle, merge queue, billing and a new shell2104 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 daily2105 let issue = pull.issue.and_then(|number| {
2106 issues
2107 .iter()
2108 .find(|issue| issue.repo_id == pull.repo_id && issue.number == number)
2109 .cloned()
2110 });
Agents as a team: lifecycle, merge queue, billing and a new shell2111 // Only a pull request g1t is seeing through has a lifecycle.
2112 let lifecycle = if !lifecycle::made_by_g1t(&pull) || snapshot.managed == 0 {
2113 None
2114 } else if let (Some(stage), Some(detail)) = (snapshot.stage, snapshot.stage_detail) {
2115 Some(Lifecycle {
2116 stage,
2117 detail,
2118 revisions: snapshot.revisions,
2119 })
2120 } else {
2121 let behind = self.is_behind(&pull.repo_id, &pull).await?;
2122 self.assess(&pull, &issue, behind)
2123 .await?
2124 .map(|(lifecycle, _)| lifecycle)
2125 };
2126 Ok::<_, worker::Error>(ActivePull {
2127 pull,
2128 issue,
2129 lifecycle,
2130 })
2131 }))
2132 .await
Work service in Rust, with RFC 3339 timestamps2133 }
2134
Issues and pull requests replace intents and attempts2135 // --- Sessions ----------------------------------------------------------
2136
Work service in Rust, with RFC 3339 timestamps2137 async fn append_session(&self, a: AppendSessionArgs) -> Result<Outcome<Appended>> {
2138 if a.entries.is_empty() {
2139 return Ok(Outcome::Ok(Appended { count: 0 }));
2140 }
2141 if a.entries.len() > MAX_ENTRY_BATCH {
2142 return Ok(Outcome::fail(
2143 FailureCode::Invalid,
2144 format!("Send at most {MAX_ENTRY_BATCH} entries at a time."),
2145 ));
2146 }
Issues and pull requests replace intents and attempts2147 let viewer = Some(a.actor.clone());
2148 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 rights2149 if !pull.is_owned_by(&a.actor.id) {
Issues and pull requests replace intents and attempts2150 return Ok(Outcome::fail(
2151 FailureCode::Forbidden,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2152 "Only whoever opened a pull request, or asked g1t for it, can record its session.",
Issues and pull requests replace intents and attempts2153 ));
2154 }
Work service in Rust, with RFC 3339 timestamps2155
2156 let now = rfc3339(now_ms());
2157 let count = a.entries.len() as u32;
2158 let mut statements = Vec::with_capacity(a.entries.len() + 1);
2159 for entry in a.entries {
2160 let kind = serde_json::to_value(entry.kind)?;
2161 let text: String = entry.text.chars().take(MAX_ENTRY_CHARS).collect();
2162 // Each insert takes the next sequence number itself, so two
2163 // writers appending at once cannot collide.
2164 statements.push(
2165 self.db
2166 .prepare(
Issues and pull requests replace intents and attempts2167 "INSERT INTO session_entries (pull_id, seq, kind, text, tool, \"commit\", at)
Work service in Rust, with RFC 3339 timestamps2168 SELECT ?, COALESCE(MAX(seq), 0) + 1, ?, ?, ?, ?, ?
Issues and pull requests replace intents and attempts2169 FROM session_entries WHERE pull_id = ?",
Work service in Rust, with RFC 3339 timestamps2170 )
2171 .bind(&[
Issues and pull requests replace intents and attempts2172 pull.id.as_str().into(),
Work service in Rust, with RFC 3339 timestamps2173 kind.as_str().unwrap_or("note").into(),
2174 text.into(),
2175 optional(&entry.tool),
Issues and pull requests replace intents and attempts2176 optional(&entry.commit.or_else(|| pull.head_commit.clone())),
Work service in Rust, with RFC 3339 timestamps2177 now.as_str().into(),
Issues and pull requests replace intents and attempts2178 pull.id.as_str().into(),
Work service in Rust, with RFC 3339 timestamps2179 ])?,
2180 );
2181 }
2182 statements.push(
2183 self.db
Issues and pull requests replace intents and attempts2184 .prepare("UPDATE pulls SET updated_at = ? WHERE id = ?")
2185 .bind(&[now.as_str().into(), pull.id.as_str().into()])?,
Work service in Rust, with RFC 3339 timestamps2186 );
2187 self.db.batch(statements).await?;
Issues and pull requests replace intents and attempts2188 self.publish(
2189 "session.appended",
2190 &pull.repo_id,
2191 &a.actor,
2192 SessionAppended {
2193 pull_id: pull.id.clone(),
2194 repo_id: pull.repo_id.clone(),
2195 number: pull.number,
Work service in Rust, with RFC 3339 timestamps2196 count,
2197 },
Issues and pull requests replace intents and attempts2198 )
Work service in Rust, with RFC 3339 timestamps2199 .await?;
2200 Ok(Outcome::Ok(Appended { count }))
2201 }
2202
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request2203 /// Adds entries to a pull request's session, each taking the next
2204 /// sequence number, without announcing it.
2205 pub(crate) async fn append_entries(&self, pull: &Pull, entries: &[NewSessionEntry]) -> Result<()> {
2206 let now = rfc3339(now_ms());
2207 let mut statements = Vec::with_capacity(entries.len());
2208 for entry in entries {
2209 let kind = serde_json::to_value(entry.kind)?;
2210 let text: String = entry.text.chars().take(MAX_ENTRY_CHARS).collect();
2211 statements.push(
2212 self.db
2213 .prepare(
2214 "INSERT INTO session_entries (pull_id, seq, kind, text, tool, \"commit\", at)
2215 SELECT ?, COALESCE(MAX(seq), 0) + 1, ?, ?, ?, ?, ?
2216 FROM session_entries WHERE pull_id = ?",
2217 )
2218 .bind(&[
2219 pull.id.as_str().into(),
2220 kind.as_str().unwrap_or("note").into(),
2221 text.into(),
2222 optional(&entry.tool),
2223 optional(&entry.commit.clone().or_else(|| pull.head_commit.clone())),
2224 now.as_str().into(),
2225 pull.id.as_str().into(),
2226 ])?,
2227 );
2228 }
2229 self.db.batch(statements).await?;
2230 Ok(())
2231 }
2232
Issues and pull requests replace intents and attempts2233 async fn read_session(&self, a: ViewArgs) -> Result<Outcome<Vec<SessionEntry>>> {
2234 let (_, pull) = check!(self.pull_at(&a.repo, a.number, &a.viewer).await?);
Work service in Rust, with RFC 3339 timestamps2235 let rows = self
2236 .db
2237 .prepare(
2238 "SELECT seq, kind, text, tool, \"commit\", at FROM session_entries
Issues and pull requests replace intents and attempts2239 WHERE pull_id = ? AND seq > ? ORDER BY seq LIMIT ?",
Work service in Rust, with RFC 3339 timestamps2240 )
Issues and pull requests replace intents and attempts2241 .bind(&[pull.id.into(), a.after_seq.into(), SESSION_PAGE.into()])?
Work service in Rust, with RFC 3339 timestamps2242 .all()
2243 .await?
2244 .results::<SessionRow>()?;
2245 Ok(Outcome::Ok(
2246 rows.into_iter().map(SessionEntry::from).collect(),
2247 ))
2248 }
2249
Events service in Rust, with RFC 3339 times and accurate push events2250 /// A push moves the head of the pull request it concerns: the one whose
2251 /// fork was pushed to, or the one opened from the branch that moved.
2252 async fn on_event(&self, event: &Event) -> Result<()> {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2253 // A new repository starts with the default labels.
2254 if event.kind == "repo.created"
2255 && let Some(repo_id) = event.repo_id.as_deref()
2256 {
2257 return self.seed_labels(repo_id).await;
2258 }
2259 // Pull requests into the branch that became the default merge into
2260 // the default branch, which is stored as none.
2261 if event.kind == "repo.default_branch_changed"
2262 && let (Some(repo_id), Some(to)) = (event.repo_id.as_deref(), event.data["to"].as_str())
2263 {
2264 self.db
2265 .prepare(
2266 "UPDATE pulls SET base_branch = NULL
2267 WHERE repo_id = ? AND base_branch = ? AND status IN ('draft', 'open')",
2268 )
2269 .bind(&[repo_id.into(), to.into()])?
2270 .run()
2271 .await?;
2272 return Ok(());
2273 }
Work service in Rust, with RFC 3339 timestamps2274 if event.kind != "git.push" {
2275 return Ok(());
2276 }
Events service in Rust, with RFC 3339 times and accurate push events2277 let (Some(repo_id), Some(after), Some(git_ref)) = (
2278 event.repo_id.as_deref(),
2279 event.data["after"].as_str(),
2280 event.data["ref"].as_str(),
2281 ) else {
Work service in Rust, with RFC 3339 timestamps2282 return Ok(());
2283 };
Pull requests from branches2284 let now = rfc3339(now_ms());
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2285 // Who moved it, for rules about the most recent push.
2286 let pusher: JsValue = event.actor.as_deref().map_or(JsValue::NULL, Into::into);
Agents as a team: lifecycle, merge queue, billing and a new shell2287 // The head moved, so whatever the checks said no longer applies, and
2288 // whatever step g1t was waiting on has been taken.
Acceptance checks in sandboxes, line comments and review verdicts2289 let moved = "UPDATE pulls
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2290 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 shell2291 working_on = NULL, working_until = NULL, stalled = NULL";
Acceptance checks in sandboxes, line comments and review verdicts2292 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 rights2293 let returning =
2294 "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 verdicts2295 let mut pulls: Vec<MovedRow> = Vec::new();
Events service in Rust, with RFC 3339 times and accurate push events2296 // A fork carries its pull request on its default branch.
2297 if event.data["defaultBranch"].as_bool() == Some(true) {
Acceptance checks in sandboxes, line comments and review verdicts2298 pulls.extend(
Events service in Rust, with RFC 3339 times and accurate push events2299 self.db
2300 .prepare(format!(
Acceptance checks in sandboxes, line comments and review verdicts2301 "{moved} WHERE fork_repo_id = ? AND {active} {returning}"
Events service in Rust, with RFC 3339 times and accurate push events2302 ))
Acceptance checks in sandboxes, line comments and review verdicts2303 .bind(&[
2304 after.into(),
2305 now.as_str().into(),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2306 pusher.clone(),
2307 now.as_str().into(),
Acceptance checks in sandboxes, line comments and review verdicts2308 repo_id.into(),
2309 after.into(),
2310 ])?
2311 .all()
2312 .await?
2313 .results::<MovedRow>()?,
Events service in Rust, with RFC 3339 times and accurate push events2314 );
2315 }
2316 if let Some(branch) = git_ref.strip_prefix("refs/heads/") {
Acceptance checks in sandboxes, line comments and review verdicts2317 pulls.extend(
Pull requests from branches2318 self.db
Events service in Rust, with RFC 3339 times and accurate push events2319 .prepare(format!(
Acceptance checks in sandboxes, line comments and review verdicts2320 "{moved} WHERE repo_id = ? AND source_branch = ? AND {active} {returning}"
Events service in Rust, with RFC 3339 times and accurate push events2321 ))
2322 .bind(&[
2323 after.into(),
2324 now.as_str().into(),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2325 pusher.clone(),
2326 now.as_str().into(),
Events service in Rust, with RFC 3339 times and accurate push events2327 repo_id.into(),
2328 branch.into(),
Acceptance checks in sandboxes, line comments and review verdicts2329 after.into(),
2330 ])?
2331 .all()
2332 .await?
2333 .results::<MovedRow>()?,
Events service in Rust, with RFC 3339 times and accurate push events2334 );
Pull requests from branches2335 }
Agents as a team: lifecycle, merge queue, billing and a new shell2336 // What each now changes, so overlaps show while the work is under way.
2337 for moved in &pulls {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2338 if let Some(mut pull) = self.pull_by_id(&moved.id).await? {
2339 pull.files = self.refresh_files(&pull).await?;
2340 // Owners of files it now changes are asked too.
2341 self.refresh_code_owners(&pull).await;
Agents as a team: lifecycle, merge queue, billing and a new shell2342 }
2343 }
2344 // A merge that was waiting for this push to bring it up to date.
2345 for moved in &pulls {
2346 self.land_if_requested(&moved.id).await?;
2347 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2348 // Whether each still merges cleanly, and, when a default branch
2349 // moved, every open pull request into it.
2350 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 bar2351 let into = match event.data["defaultBranch"].as_bool() {
2352 Some(true) => mergeability::Moved::DefaultBranch,
2353 _ => match git_ref.strip_prefix("refs/heads/") {
2354 Some(branch) => mergeability::Moved::Branch(branch),
2355 None => mergeability::Moved::Nothing,
2356 },
2357 };
2358 self.after_push(repo_id, into, &moved_ids).await;
Acceptance checks in sandboxes, line comments and review verdicts2359 // A draft is announced when it is marked ready instead.
2360 for pull in pulls
2361 .into_iter()
2362 .filter(|pull| pull.status == PullStatus::Open)
2363 {
2364 self.publish_as(
2365 "pull.updated",
2366 &pull.repo_id,
2367 event.actor.clone(),
2368 PullEvent {
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2369 author: Some(g1t_contracts::credentials::Principal { id: pull.author_id, username: pull.author_name }),
2370 requested_by: pull
2371 .requested_by_id
2372 .zip(pull.requested_by_name)
2373 .map(|(id, username)| g1t_contracts::credentials::Principal { id, username }),
Acceptance checks in sandboxes, line comments and review verdicts2374 pull_id: pull.id,
2375 repo_id: pull.repo_id.clone(),
2376 number: pull.number,
2377 issue: pull.issue_number,
2378 commit: Some(after.to_owned()),
2379 ..PullEvent::default()
2380 },
2381 )
2382 .await?;
2383 }
Work service in Rust, with RFC 3339 timestamps2384 Ok(())
2385 }
2386}
2387
2388fn service(env: &Env) -> Result<Work> {
2389 Ok(Work {
2390 db: env.d1("DB")?,
Agents as a team: lifecycle, merge queue, billing and a new shell2391 identity: env.service("IDENTITY")?,
Work service in Rust, with RFC 3339 timestamps2392 repos: env.service("REPOS")?,
Events service in Rust, with RFC 3339 times and accurate push events2393 events: env.service("EVENTS")?,
Sidebar: the panels really slide2394 actions: env.service("ACTIONS")?,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2395 timing: g1t_kit::d1::Timing::default(),
2396 prefetched: std::cell::RefCell::new(None),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2397 known_repos: std::cell::RefCell::new(std::collections::HashMap::new()),
Work service in Rust, with RFC 3339 timestamps2398 })
2399}
2400
2401#[event(fetch)]
2402async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
2403 let Some(method) = rpc_method(&request) else {
2404 return Response::error("Not found", 404);
2405 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents2406 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
2407 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
Work service in Rust, with RFC 3339 timestamps2408 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents2409 let mut work = service(&env)?;
2410 work.db = db;
Work service in Rust, with RFC 3339 timestamps2411
Fast pages, required checks on the branch, self-hosted runners, honest incidents2412 let answered = match method.as_str() {
Issues and pull requests replace intents and attempts2413 "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 step2414 "delegate_issue" => reply(&work.delegate_issue(args(body)?).await?),
2415 "report_confidence" => reply(&work.report_confidence(args(body)?).await?),
Issues and pull requests replace intents and attempts2416 "list_issues" => reply(&work.list_issues(args(body)?).await?),
2417 "get_issue" => reply(&work.get_issue(args(body)?).await?),
2418 "update_issue" => reply(&work.update_issue(args(body)?).await?),
2419 "close_issue" => reply(&work.close_issue(args(body)?).await?),
2420 "reopen_issue" => reply(&work.reopen_issue(args(body)?).await?),
2421 "list_labels" => reply(&work.list_labels(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2422 "save_label" => reply(&work.save_label(args(body)?).await?),
2423 "delete_label" => reply(&work.delete_label(args(body)?).await?),
2424 "add_default_labels" => reply(&work.add_default_labels(args(body)?).await?),
2425 "set_labels" => reply(&work.set_labels(args(body)?).await?),
2426 "list_milestones" => reply(&work.list_milestones(args(body)?).await?),
2427 "get_milestone" => reply(&work.get_milestone(args(body)?).await?),
2428 "save_milestone" => reply(&work.save_milestone(args(body)?).await?),
2429 "delete_milestone" => reply(&work.delete_milestone(args(body)?).await?),
Issues and pull requests replace intents and attempts2430 "counts" => reply(&work.counts(args(body)?).await?),
2431 "add_comment" => reply(&work.add_comment(args(body)?).await?),
Acceptance checks in sandboxes, line comments and review verdicts2432 "start_checks" => reply(&work.start_checks(args(body)?).await?),
Fast pages, required checks on the branch, self-hosted runners, honest incidents2433 "seen_checks" => reply(&work.seen_checks(args(body)?).await?),
Acceptance checks in sandboxes, line comments and review verdicts2434 "report_checks" => reply(&work.report_checks(args(body)?).await?),
GitHub Actions on g1t, part two: running workflows2435 "set_commit_status" => reply(&work.set_commit_status(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2436 "start_review" => reply(&work.start_review(args(body)?).await?),
2437 "advance" => reply(&work.advance(args(body)?).await?),
2438 "stall" => reply(&work.stall(args(body)?).await?),
2439 "managed_pulls" => reply(&work.managed_pulls(args(body)?).await?),
2440 "queue" => reply(&work.queue(args(body)?).await?),
2441 "queue_build" => reply(&work.queue_build(args(body)?).await?),
2442 "report_queue" => reply(&work.report_queue(args(body)?).await?),
2443 "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 request2444 "message_agent" => reply(&work.message_agent(args(body)?).await?),
Record your own agent's sessions automatically2445 "locate_pull" => reply(&work.locate_pull(args(body)?).await?),
Inbox: the events service tells people what needs them as events arrive2446 "inbox_subject" => reply(&work.inbox_subject(args(body)?).await?),
Agents ask each other, hand each other work, and answer2447 "answer_message" => reply(&work.answer_message(args(body)?).await?),
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request2448 "take_messages" => reply(&work.take_messages(args(body)?).await?),
Agents asked while not at work are woken to answer2449 "wake_for_messages" => reply(&work.wake_for_messages(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2450 "catch_up_job" => reply(&work.catch_up_job(args(body)?).await?),
2451 "get_settings" => reply(&work.get_settings(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2452 "codeowners_errors" => reply(&work.codeowners_errors(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2453 "update_settings" => reply(&work.update_settings(args(body)?).await?),
2454 "report_review" => reply(&work.report_review(args(body)?).await?),
Issues and pull requests replace intents and attempts2455 "open_pull" => reply(&work.open_pull(args(body)?).await?),
2456 "list_pulls" => reply(&work.list_pulls(args(body)?).await?),
Fast pages, required checks on the branch, self-hosted runners, honest incidents2457 "pulls_for_repos" => reply(&work.pulls_for_repos(args(body)?).await?),
Issues and pull requests replace intents and attempts2458 "get_pull" => reply(&work.get_pull(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2459 "update_pull" => reply(&work.update_pull(args(body)?).await?),
Catching up with main takes seconds when the two sides touched different files2460 "catch_up_pull" => reply(&work.catch_up_pull(args(body)?).await?),
Issues and pull requests replace intents and attempts2461 "ready_pull" => reply(&work.ready_pull(args(body)?).await?),
2462 "close_pull" => reply(&work.close_pull(args(body)?).await?),
2463 "merge_pull" => reply(&work.merge_pull(args(body)?).await?),
2464 "list_active_pulls" => reply(&work.list_active_pulls(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2465 "by_author" => reply(&work.by_author(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell2466 "start_plan" => reply(&work.start_plan(args(body)?).await?),
2467 "report_plan" => reply(&work.report_plan(args(body)?).await?),
2468 "get_plan" => reply(&work.get_plan(args(body)?).await?),
2469 "list_plans" => reply(&work.list_plans(args(body)?).await?),
2470 "apply_plan" => reply(&work.apply_plan(args(body)?).await?),
2471 "queue_issue" => reply(&work.queue_issue(args(body)?).await?),
2472 "ready_issues" => reply(&work.ready_issues(args(body)?).await?),
2473 "list_assigned_issues" => reply(&work.list_assigned_issues(args(body)?).await?),
Work service in Rust, with RFC 3339 timestamps2474 "append_session" => reply(&work.append_session(args(body)?).await?),
2475 "read_session" => reply(&work.read_session(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2476 // Agents at work, their sessions, and memory (runs.rs, memory.rs).
2477 "open_run" => reply(&work.open_run(args(body)?).await?),
2478 "report_run" => reply(&work.report_run(args(body)?).await?),
2479 "stop_run" => reply(&work.stop_run(args(body)?).await?),
2480 "list_runs" => reply(&work.list_runs(args(body)?).await?),
2481 "get_run" => reply(&work.get_run(args(body)?).await?),
2482 "list_sessions" => reply(&work.list_sessions(args(body)?).await?),
2483 "get_session" => reply(&work.get_session(args(body)?).await?),
2484 "list_memories" => reply(&work.list_memories(args(body)?).await?),
2485 "add_memory" => reply(&work.add_memory(args(body)?).await?),
2486 "update_memory" => reply(&work.update_memory(args(body)?).await?),
2487 "delete_memory" => reply(&work.delete_memory(args(body)?).await?),
2488 "recall" => reply(&work.recall(args(body)?).await?),
2489 "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 API2490 // What agents may do in a sandbox (guardrails.rs).
2491 "get_guardrails" => reply(&work.get_guardrails(args(body)?).await?),
2492 "update_guardrails" => reply(&work.update_guardrails(args(body)?).await?),
2493 "run_guardrails" => reply(&work.run_guardrails(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2494 // Plan caps the runner applies (compute.rs).
2495 "active_agents" => reply(&work.active_agents(args(body)?).await?),
2496 "issue_spend" => reply(&work.issue_spend(args(body)?).await?),
2497 "wait_for_slot" => reply(&work.wait_for_slot(args(body)?).await?),
2498 "agent_comment" => reply(&work.agent_comment(args(body)?).await?),
2499 "add_wait" => reply(&work.add_wait(args(body)?).await?),
2500 "waiting_workspaces" => reply(&work.waiting_workspaces(args(body)?).await?),
2501 "take_wait" => reply(&work.take_wait(args(body)?).await?),
2502 // The runs whose sandboxes stop with their repository (retired.rs).
2503 "runs_in_repo" => reply(&work.runs_in_repo(args(body)?).await?),
2504 "run_cost" => reply(&work.run_cost(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2505 "start_mergecheck" => reply(&work.start_mergecheck(args(body)?).await?),
2506 "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 API2507 // Memory that fills itself, and its review queue (capture.rs).
2508 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-agent2509 // @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 API2510 "take_mention" => reply(&work.take_mention(args(body)?).await?),
2511 "mention_revision" => reply(&work.mention_revision(args(body)?).await?),
2512 "reply_mention" => reply(&work.reply_mention(args(body)?).await?),
2513 "get_agent_rules" => reply(&work.get_agent_rules(args(body)?).await?),
2514 "set_agent_rules" => reply(&work.set_agent_rules(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge2515 // Rulesets (rulesets.rs): kept here, enforced here on merge and by
2516 // repos on push.
2517 "list_rulesets" => reply(&work.list_rulesets(args(body)?).await?),
2518 "get_ruleset" => reply(&work.get_ruleset(args(body)?).await?),
2519 "save_ruleset" => reply(&work.save_ruleset(args(body)?).await?),
2520 "delete_ruleset" => reply(&work.delete_ruleset(args(body)?).await?),
2521 "effective_rules" => reply(&work.effective_rules(args(body)?).await?),
2522 "rule_evaluations" => reply(&work.rule_evaluations(args(body)?).await?),
2523 "ref_rules" => reply(&work.ref_rules(args(body)?).await?),
2524 "record_evaluations" => reply(&work.record_evaluations(args(body)?).await?),
2525 "set_requires_pull_request" => reply(&work.set_requires_pull_request(args(body)?).await?),
Work service in Rust, with RFC 3339 timestamps2526 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents2527 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2528 served.finish_timed(answered, &work.timing)
Work service in Rust, with RFC 3339 timestamps2529}
2530
2531/// Events from the bus, delivered on this service's own queue.
2532#[event(queue)]
Events service in Rust, with RFC 3339 times and accurate push events2533async fn queue(batch: MessageBatch<Event>, env: Env, _ctx: Context) -> Result<()> {
Work service in Rust, with RFC 3339 timestamps2534 let work = service(&env)?;
2535 for message in batch.messages()? {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains2536 // 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 merge2537 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 domains2538 message.ack();
2539 continue;
2540 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2541 // A repository renamed or transferred: its runs, memory, guardrails
2542 // and runs waiting for a slot follow.
2543 if g1t_kit::transfer::on_event(&env, &env.d1("DB")?, message.body(), &[memory::TRANSFERRED, guardrails::TRANSFERRED, retired::WAITS_MOVED].concat()).await? {
2544 message.ack();
2545 continue;
2546 }
2547 // A workspace deleted: what it kept for itself goes.
2548 if g1t_kit::deleted::on_event(&env.d1("DB")?, message.body(), memory::DELETED).await? {
2549 message.ack();
2550 continue;
2551 }
2552 // A repository deleted, archived or purged, or a branch renamed (retired.rs).
2553 if work.on_retired(message.body()).await? {
2554 message.ack();
2555 continue;
2556 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API2557 capture::on_event(&work, message.body()).await;
Work service in Rust, with RFC 3339 timestamps2558 work.on_event(message.body()).await?;
2559 message.ack();
2560 }
2561 Ok(())
2562}
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights2563
2564/// The rules that once read a pull request's author read its owner now:
2565/// whoever asked g1t for it, or its author. For each, the person who asked
2566/// is held to what an author was, and g1t's agent (a token it works with)
2567/// gains nothing by being the author.
2568#[cfg(test)]
2569mod owner_rules {
2570 use super::*;
2571 use crate::rows::stored::{ASKER, G1T, pull};
2572 use g1t_contracts::identity::AGENT_ID;
2573
2574 const SOMEONE: &str = "usr_2";
2575
2576 #[test]
2577 fn no_self_approval() {
2578 // add_comment refuses a verdict on one that is theirs.
2579 let made = pull(G1T, Some(ASKER));
2580 assert!(made.is_owned_by(ASKER.0), "the person who asked cannot approve it");
2581 assert!(!made.is_owned_by(AGENT_ID), "g1t's review agent still gives its verdict");
2582 assert!(!made.is_owned_by(SOMEONE));
2583 }
2584
2585 #[test]
2586 fn what_an_author_could_do_without_a_role() {
2587 // manageable_pull (update, ready, close), catch_up_pull on a fork,
2588 // append_session, and steering with message_agent: theirs to do.
2589 let made = pull(G1T, Some(ASKER));
2590 assert!(made.is_owned_by(ASKER.0));
2591 assert!(!made.is_owned_by(SOMEONE), "anyone else still needs the role");
2592 assert!(!made.is_owned_by(AGENT_ID), "being its author gives g1t's tokens nothing more");
2593 }
2594
2595 #[test]
2596 fn nobody_is_asked_to_review_what_they_asked_for() {
2597 // update_pull drops the owner from the reviewers asked.
2598 let made = pull(G1T, Some(ASKER));
2599 let mut reviewers = vec!["syntaqx".to_owned(), "ana".to_owned()];
2600 reviewers.retain(|name| *name != made.owner().username);
2601 assert_eq!(reviewers, ["ana"]);
2602 }
2603
2604 #[test]
2605 fn sandboxes_act_as_whoever_asked() {
2606 // LifecycleJob, ReviewJob, MergecheckJob and the merge queue's job
2607 // carry who the sandbox's credential acts for: a real account.
2608 let made = pull(G1T, Some(ASKER));
2609 assert_eq!(made.owner().id, ASKER.0);
2610 let acts_as = made.requested_by.unwrap_or(made.author);
2611 assert_eq!(acts_as.id, ASKER.0);
2612 // g1t's own work, which nobody asked for, acts as g1t, as before.
2613 let own = pull(("g1t", "g1t"), None);
2614 assert_eq!(own.requested_by.unwrap_or(own.author).id, "g1t");
2615 }
2616
2617 #[test]
2618 fn events_name_g1t_and_whoever_asked() {
2619 let made = pull(G1T, Some(ASKER));
2620 let event = serde_json::to_value(Work::pull_event(&made)).unwrap();
2621 assert_eq!(event["author"], serde_json::json!({ "id": AGENT_ID, "username": "g1t" }));
2622 assert_eq!(event["requestedBy"], serde_json::json!({ "id": "usr_1", "username": "syntaqx" }));
2623 let own = serde_json::to_value(Work::pull_event(&pull(ASKER, None))).unwrap();
2624 assert!(own.get("requestedBy").is_none());
2625 }
2626}

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