Skip to content
1,463 linesCodeBlameRaw

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

The keeper reads Cloudflare as it really answers1//! Events published on the bus, and the events service that carries them.
2//! Mirrors `packages/contracts/src/events.ts`.
3
4use serde::Serialize;
5
Merge branch 'worktree-agent-a3abfcce648e87dca'6/// The key in an event's `data` that marks what a workflow job's own token
7/// (`G1T_TOKEN`) did, with the run's id as its value. The actions service
8/// starts no workflows for such an event, as GitHub starts none for what
9/// its `GITHUB_TOKEN` does, so a workflow cannot set itself off.
10pub const CAUSED_BY_JOB: &str = "causedByJob";
11
12/// The run whose job's token caused an event, if one did.
13pub fn caused_by_job(data: &serde_json::Value) -> Option<&str> {
14 data[CAUSED_BY_JOB].as_str().filter(|run| !run.is_empty())
15}
16
17/// The run whose job's token `actor` is acting with, if it is one.
18pub fn job_run_of(actor: &crate::User) -> Option<&str> {
19 actor.token.as_deref().and_then(|token| token.job.as_ref()).map(|job| job.run_id.as_str())
20}
21
22/// `data` as JSON, marked as a workflow job's doing when `actor` acted
23/// with a job's token (see [`CAUSED_BY_JOB`]).
24pub fn marked<T: Serialize>(data: T, actor: Option<&crate::User>) -> serde_json::Value {
25 let mut value = serde_json::to_value(data).unwrap_or(serde_json::Value::Null);
26 if let (Some(run), serde_json::Value::Object(map)) = (actor.and_then(job_run_of), &mut value) {
27 map.insert(CAUSED_BY_JOB.to_owned(), serde_json::Value::String(run.to_owned()));
28 }
29 value
30}
31
32/// `data` as JSON, marked as a workflow job's doing when the event it
33/// follows from (`cause`, its data) was: a push by a job's token moves its
34/// pull request, and that starts no workflows either.
35pub fn carried<T: Serialize>(data: T, cause: &serde_json::Value) -> serde_json::Value {
36 let mut value = serde_json::to_value(data).unwrap_or(serde_json::Value::Null);
37 if let (Some(run), serde_json::Value::Object(map)) = (caused_by_job(cause), &mut value) {
38 map.insert(CAUSED_BY_JOB.to_owned(), serde_json::Value::String(run.to_owned()));
39 }
40 value
41}
42
The keeper reads Cloudflare as it really answers43/// What a publisher supplies; the bus fills in the id and time.
44#[derive(Debug, Serialize)]
45#[serde(rename_all = "camelCase")]
46pub struct NewEvent<T: Serialize> {
47 #[serde(rename = "type")]
48 pub kind: &'static str,
49 /// The service that published it.
50 pub source: &'static str,
51 /// The repo the event concerns.
52 pub repo_id: Option<String>,
53 /// The user or agent that caused it, if any.
54 pub actor: Option<String>,
55 pub data: T,
56}
57
58#[derive(Debug, Serialize)]
59#[serde(rename_all = "camelCase")]
60pub struct RepoCreated {
61 pub repo_id: String,
62 pub namespace: String,
63 pub name: String,
64 pub is_private: bool,
65}
66
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look67/// The payload of `repo.collaborator_added`, `repo.collaborator_removed`
68/// and `repo.collaborator_role_changed`: a person's own role on one
69/// repository (see `access`). `role` is the role they have now (null once
70/// removed); `previous_role` what they had before (null when added).
The keeper reads Cloudflare as it really answers71#[derive(Debug, Serialize)]
72#[serde(rename_all = "camelCase")]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look73pub struct RepoCollaborator {
74 pub repo_id: String,
75 pub namespace: String,
76 pub name: String,
77 pub username: String,
78 pub role: Option<crate::access::RepoRole>,
79 pub previous_role: Option<crate::access::RepoRole>,
80}
81
82#[derive(Debug, Serialize)]
83#[serde(rename_all = "camelCase")]
The keeper reads Cloudflare as it really answers84pub struct RepoForked {
85 pub repo_id: String,
86 pub source_repo_id: String,
87 pub pull_id: String,
88}
89
90/// One branch or tag moved by a push. `after` is the commit it points to now.
91#[derive(Debug, Serialize)]
92#[serde(rename_all = "camelCase")]
93pub struct GitPush {
94 pub repo_id: String,
95 /// The full ref, such as `refs/heads/main`.
96 #[serde(rename = "ref")]
97 pub git_ref: String,
98 /// Where it pointed before; absent for a new branch or tag.
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub before: Option<String>,
101 pub after: String,
102 /// Whether the ref is the repository's default branch.
103 pub default_branch: bool,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily104 /// Set when the push was too large to scan for secrets before it was
105 /// stored, and was let through: the security service scans
106 /// `before..after` after it landed. Absent otherwise.
107 #[serde(skip_serializing_if = "std::ops::Not::not")]
108 pub unscanned: bool,
Merge branch 'worktree-agent-a3abfcce648e87dca'109 /// Set when a workflow job's token pushed: the run's id (see
110 /// [`CAUSED_BY_JOB`]). Absent otherwise.
111 #[serde(rename = "causedByJob", skip_serializing_if = "Option::is_none")]
112 pub caused_by_job: Option<String>,
Merge branch 'mirroring' into artifacts-mode113 /// Set when the push was copied in from the remote a mirror follows,
114 /// not made on g1t. Absent otherwise.
115 #[serde(skip_serializing_if = "std::ops::Not::not")]
116 pub mirrored: bool,
117 /// The repository's mirror state when the push landed (see
118 /// [`crate::mirrors`]): workflows and deployments follow it. Absent for
119 /// a repository that leads.
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub mirror: Option<crate::mirrors::RepoMirror>,
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.122 /// How many commits the push brought to the branch, counted along its
123 /// first-parent line from `after` back to `before` (a merge commit is
124 /// one), at most 50; one for a new branch. Absent when it was not
125 /// counted: a tag, a branch deleted, a pull request landed, a push the
126 /// store could not read back, or one from before this was recorded.
127 /// Home's digest (`activity_digest`) sums it.
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub commits: Option<u32>,
The keeper reads Cloudflare as it really answers130}
131
132/// The payload of `issue.opened`, `issue.updated`, `issue.assigned`,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar133/// `issue.labeled`, `issue.unlabeled`, `issue.milestoned`,
134/// `issue.demilestoned`, `issue.closed` and `issue.reopened`; each uses
135/// the fields that apply to it.
The keeper reads Cloudflare as it really answers136#[derive(Debug, Default, Serialize)]
137#[serde(rename_all = "camelCase")]
138pub struct IssueEvent {
139 pub issue_id: String,
140 pub repo_id: String,
141 pub number: u32,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights142 /// Who opened it: g1t, for one its agent filed while at work.
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub author: Option<crate::credentials::Principal>,
145 /// For an issue g1t's agent filed: the person it was working for.
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub requested_by: Option<crate::credentials::Principal>,
The keeper reads Cloudflare as it really answers148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub title: Option<String>,
150 /// On close: `completed` or `not_planned`.
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub reason: Option<&'static str>,
153 /// On close: the number of the pull request whose merge closed it.
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub resolved_by: Option<u32>,
156 /// On `issue.assigned`: the people it is now assigned to.
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub assignees: Option<Vec<String>>,
Events: review requests, assignments, stops and deployments are published159 /// On `issue.assigned`: those of them who were not before.
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub added: Option<Vec<String>>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar162 /// On `issue.labeled` and `issue.unlabeled`: the label put on or taken
163 /// off. One event for each.
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub label: Option<EventLabel>,
166 /// On `issue.milestoned`: the milestone it was put in; on
167 /// `issue.demilestoned`, the one it was taken out of.
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub milestone: Option<crate::work::MilestoneRef>,
The keeper reads Cloudflare as it really answers170}
171
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar172/// A label, as `issue.labeled`, `pull.labeled` and their `unlabeled` say.
173#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
174pub struct EventLabel {
175 pub name: String,
176 /// Six hex digits.
177 pub color: String,
178}
179
The keeper reads Cloudflare as it really answers180/// The payload of `pull.opened`, `pull.ready`, `pull.updated` (its head
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts181/// moved), `pull.closed`, `pull.reopened`, `pull.converted_to_draft`,
182/// `pull.merged`, `pull.assigned`,
Events: review requests, assignments, stops and deployments are published183/// `pull.review_requested` and `pull.review_request_removed` (reviewers
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar184/// asked, or no longer), `pull.labeled` and `pull.unlabeled`,
185/// `pull.milestoned` and `pull.demilestoned`, `pull.base_changed` (the
186/// branch it merges into changed), `pull.stalled` (g1t stopped seeing it
187/// through until a person steps in) and `pull.resumed` (it picked back
188/// up); each uses the fields that apply to it.
The keeper reads Cloudflare as it really answers189#[derive(Debug, Default, Serialize)]
190#[serde(rename_all = "camelCase")]
191pub struct PullEvent {
192 pub pull_id: String,
193 pub repo_id: String,
194 pub number: u32,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights195 /// Who opened it: g1t, for a change g1t made.
196 #[serde(skip_serializing_if = "Option::is_none")]
197 pub author: Option<crate::credentials::Principal>,
198 /// For a change g1t made: the person who asked for it.
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub requested_by: Option<crate::credentials::Principal>,
The keeper reads Cloudflare as it really answers201 /// The number of the issue it is for.
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub issue: Option<u32>,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub agent: Option<String>,
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts206 /// On merge: the commit the branch now points to. On update, when
207 /// marked ready and when reopened: the head of the change.
The keeper reads Cloudflare as it really answers208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub commit: Option<String>,
210 /// On close: the pull request that was merged instead.
211 #[serde(skip_serializing_if = "Option::is_none")]
212 pub superseded_by: Option<u32>,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step213 /// How sure g1t is of a g1t agent's change, once it has worked that out.
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub confidence: Option<crate::work::Confidence>,
Events: review requests, assignments, stops and deployments are published216 /// On `pull.assigned`: the people it is now assigned to.
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub assignees: Option<Vec<String>>,
219 /// On `pull.assigned`: those newly assigned.
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub added: Option<Vec<String>>,
222 /// On `pull.review_requested`: the reviewers newly asked; on
223 /// `pull.review_request_removed`, those no longer asked.
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub reviewers: Option<Vec<String>>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar226 /// On `pull.review_requested` and `pull.review_request_removed`: the
227 /// teams newly asked, or no longer, each with the people it asks.
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub teams: Option<Vec<TeamRequested>>,
230 /// On `pull.review_requested`: asked because they own files it changes
231 /// (its CODEOWNERS file), not by a person.
232 #[serde(skip_serializing_if = "std::ops::Not::not")]
233 pub code_owners: bool,
Events: review requests, assignments, stops and deployments are published234 /// On `pull.stalled`: why g1t stopped, and what would start it again.
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub detail: Option<String>,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar237 /// On `pull.labeled` and `pull.unlabeled`: the label put on or taken off.
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub label: Option<EventLabel>,
240 /// On `pull.milestoned`: the milestone it was put in; on
241 /// `pull.demilestoned`, the one it was taken out of.
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub milestone: Option<crate::work::MilestoneRef>,
244 /// On `pull.opened` and `pull.base_changed`: the branch it merges into.
245 #[serde(skip_serializing_if = "Option::is_none")]
246 pub base: Option<String>,
247}
248
249/// A team asked to review a pull request.
250#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, serde::Deserialize)]
251#[serde(rename_all = "camelCase")]
252pub struct TeamRequested {
253 /// `workspace/slug`.
254 pub team: String,
255 /// Everyone in it who is told: the whole team, or, with review
256 /// assignment, the people picked (and the rest when it says to tell
257 /// them). Never the pull request's author.
258 pub notified: Vec<String>,
259 /// With review assignment: the people picked, who are asked as
260 /// reviewers themselves.
261 #[serde(default)]
262 pub assigned: Vec<String>,
263}
264
265/// The payload of every `team.*` event: `team.created`, `team.edited`,
266/// `team.deleted`; `team.member_added`, `team.member_role_changed`,
267/// `team.member_removed` (with `username`, `role` and `previousRole`);
268/// and `team.repo_added`, `team.repo_role_changed`, `team.repo_removed`
269/// (with `repoId`, `repo`, `repoRole` and `previousRepoRole`), which also
270/// name the repository as the event's own.
271#[derive(Clone, Debug, Default, Serialize)]
272#[serde(rename_all = "camelCase")]
273pub struct TeamChanged {
274 pub workspace: String,
275 pub team_id: String,
276 /// The team's slug, as it is now.
277 pub team: String,
278 pub name: String,
279 pub visibility: Option<crate::teams::TeamVisibility>,
280 /// The parent's slug.
281 #[serde(skip_serializing_if = "Option::is_none")]
282 pub parent: Option<String>,
283 /// On `team.edited`: what changed, such as `name` or `parent`.
284 #[serde(skip_serializing_if = "Vec::is_empty")]
285 pub changes: Vec<String>,
286 #[serde(skip_serializing_if = "Option::is_none")]
287 pub username: Option<String>,
288 #[serde(skip_serializing_if = "Option::is_none")]
289 pub role: Option<crate::teams::TeamRole>,
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub previous_role: Option<crate::teams::TeamRole>,
292 #[serde(skip_serializing_if = "Option::is_none")]
293 pub repo_id: Option<String>,
294 /// `workspace/name`.
295 #[serde(skip_serializing_if = "Option::is_none")]
296 pub repo: Option<String>,
297 #[serde(skip_serializing_if = "Option::is_none")]
298 pub repo_role: Option<crate::access::RepoRole>,
299 #[serde(skip_serializing_if = "Option::is_none")]
300 pub previous_repo_role: Option<crate::access::RepoRole>,
Events: review requests, assignments, stops and deployments are published301}
302
303/// `deployment.succeeded` and `deployment.failed`: a build of a project
304/// finished, for production or for one pull request's preview.
305#[derive(Debug, Serialize)]
306#[serde(rename_all = "camelCase")]
307pub struct DeploymentEvent {
308 pub deployment_id: String,
309 pub project_id: String,
310 pub repo_id: String,
311 pub workspace: String,
312 /// The project's slug.
313 pub project: String,
314 /// `production` or `preview`.
315 pub kind: String,
316 pub branch: Option<String>,
317 /// For a preview: its pull request.
318 pub number: Option<u32>,
319 pub commit: String,
320 /// Where the deployment is on the site, such as
321 /// `/acme/rocket/deployments/dpl_1`.
322 pub path: String,
323 /// For a failure: what went wrong.
324 pub error: Option<String>,
325 /// For a success: whether the deployment before it, of the same app, failed.
326 pub recovered: bool,
327 /// Who started it, by username, or `g1t`.
328 pub triggered_by: String,
The keeper reads Cloudflare as it really answers329}
330
331/// `checks.completed`: a run of an issue's acceptance checks against a pull
332/// request finished.
333#[derive(Debug, Serialize)]
334#[serde(rename_all = "camelCase")]
335pub struct ChecksEvent {
336 pub pull_id: String,
337 pub repo_id: String,
338 pub number: u32,
339 /// `passed`, `failed` or `errored`.
340 pub status: &'static str,
341 /// The commit that was checked.
342 pub commit: String,
343}
344
345/// `workflow.completed`: a GitHub Actions run finished.
346#[derive(Debug, Serialize)]
347#[serde(rename_all = "camelCase")]
348pub struct WorkflowEvent {
349 pub run_id: String,
350 pub repo_id: String,
351 /// The workflow's name, and its file.
352 pub workflow: String,
353 pub path: String,
354 /// The run's number among the workflow's runs.
355 pub number: u64,
356 /// The GitHub event that started it, such as `push`.
357 pub event: String,
358 /// `success`, `failure`, `cancelled` or `skipped`.
359 pub conclusion: String,
360 #[serde(rename = "ref")]
361 pub git_ref: String,
362 pub sha: String,
363 /// The pull request it ran for, if any.
364 #[serde(skip_serializing_if = "Option::is_none")]
365 pub pull: Option<u32>,
366}
367
368/// `review.completed`: a g1t agent finished reviewing a pull request, or
369/// could not.
370#[derive(Debug, Serialize)]
371#[serde(rename_all = "camelCase")]
372pub struct ReviewEvent {
373 pub pull_id: String,
374 pub repo_id: String,
375 pub number: u32,
376 /// `approve` or `request_changes`; absent when no review was written.
377 #[serde(skip_serializing_if = "Option::is_none")]
378 pub verdict: Option<&'static str>,
379}
380
381/// `comment.created`. `number` is the issue or pull request commented on.
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar382#[derive(Debug, Default, Serialize)]
The keeper reads Cloudflare as it really answers383#[serde(rename_all = "camelCase")]
384pub struct CommentCreated {
385 pub comment_id: String,
386 pub repo_id: String,
387 pub number: u32,
388 /// Set when the comment is on a pull request.
389 #[serde(skip_serializing_if = "Option::is_none")]
390 pub pull_id: Option<String>,
391 /// Set when the comment is a review: approve or request changes.
392 #[serde(skip_serializing_if = "Option::is_none")]
393 pub verdict: Option<crate::work::Verdict>,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar394 /// Set when one of the workspace's agents wrote it, as itself. The
395 /// event's actor is then the person it acted for (`actingFor`).
396 #[serde(skip_serializing_if = "Option::is_none")]
397 pub agent: Option<crate::work::AgentRef>,
398 /// Who the agent acted for, set with `agent`.
399 #[serde(skip_serializing_if = "Option::is_none")]
400 pub acting_for: Option<crate::credentials::Principal>,
401 /// An agent's review, advisory: its verdict counts toward nothing.
402 #[serde(skip_serializing_if = "std::ops::Not::not")]
403 pub advisory: bool,
The keeper reads Cloudflare as it really answers404}
405
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts406/// `comment.edited`: a comment's text changed. `changes.body.from` is what
407/// it said before; the comment as it is now is read by `commentId`.
408#[derive(Debug, Serialize)]
409#[serde(rename_all = "camelCase")]
410pub struct CommentEdited {
411 pub comment_id: String,
412 pub repo_id: String,
413 pub number: u32,
414 /// Set when the comment is on a pull request.
415 #[serde(skip_serializing_if = "Option::is_none")]
416 pub pull_id: Option<String>,
417 pub changes: CommentChanges,
418}
419
420/// What an edit changed, as `comment.edited` says it.
421#[derive(Debug, Serialize)]
422pub struct CommentChanges {
423 pub body: ChangedFrom,
424}
425
426/// A field's value before a change.
427#[derive(Debug, Serialize)]
428pub struct ChangedFrom {
429 pub from: String,
430}
431
432/// `comment.deleted`: a comment was deleted. It no longer exists when the
433/// event is read, so `comment` is the comment as it was.
434#[derive(Debug, Serialize)]
435#[serde(rename_all = "camelCase")]
436pub struct CommentDeleted {
437 pub comment_id: String,
438 pub repo_id: String,
439 pub number: u32,
440 /// Set when the comment was on a pull request.
441 #[serde(skip_serializing_if = "Option::is_none")]
442 pub pull_id: Option<String>,
443 pub comment: DeletedComment,
444}
445
446/// A deleted comment, as it was.
The keeper reads Cloudflare as it really answers447#[derive(Debug, Serialize)]
448#[serde(rename_all = "camelCase")]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts449pub struct DeletedComment {
450 pub id: String,
451 pub body: String,
452 pub author: crate::credentials::Principal,
453 /// RFC 3339.
454 pub created_at: String,
455 /// For a comment on one line of a pull request's change: the file.
456 pub path: Option<String>,
457 /// And the line, as numbered after the change.
458 pub line: Option<u32>,
459}
460
461/// The payload of every `release.*` event: `release.created`,
462/// `release.published`, `release.released`, `release.prereleased`,
463/// `release.edited`, `release.unpublished` and `release.deleted`. One change
464/// to a release can be several of them (see [`release_actions`]).
465#[derive(Debug, Serialize)]
466#[serde(rename_all = "camelCase")]
467pub struct ReleaseEvent {
468 pub release_id: String,
469 pub repo_id: String,
470 pub tag_name: String,
471 /// As it is now; as it was, for `release.deleted`.
472 pub release: crate::about::Release,
473 /// On `release.edited`: `{ "name": { "from" }, "body": { "from" } }`
474 /// for what changed.
475 #[serde(skip_serializing_if = "Option::is_none")]
476 pub changes: Option<serde_json::Value>,
477}
478
479/// A release's state, for [`release_actions`]: whether it is a draft, and
480/// whether it is a prerelease.
481#[derive(Clone, Copy, Debug, PartialEq, Eq)]
482pub struct ReleaseState {
483 pub draft: bool,
484 pub prerelease: bool,
485}
486
487/// What a change to a release is, as GitHub's release activity types, in
488/// the order they are sent: from `before` (None: it did not exist) to
489/// `after` (None: deleted). A release made published is `created`,
490/// `published` and `released` (or `prereleased`); a draft published is
491/// `published` and `released` (or `prereleased`); made a draft again,
492/// `unpublished`; anything else `edited`, and a prerelease made a full
493/// release is `released` too.
494pub fn release_actions(before: Option<ReleaseState>, after: Option<ReleaseState>) -> Vec<&'static str> {
495 let published = |state: ReleaseState| if state.prerelease { "prereleased" } else { "released" };
496 match (before, after) {
497 (None, None) => Vec::new(),
498 (None, Some(after)) if after.draft => vec!["created"],
499 (None, Some(after)) => vec!["created", "published", published(after)],
500 (Some(_), None) => vec!["deleted"],
501 (Some(before), Some(after)) if before.draft && !after.draft => vec!["published", published(after)],
502 (Some(before), Some(after)) if !before.draft && after.draft => vec!["unpublished"],
503 (Some(before), Some(after)) if !after.draft && before.prerelease && !after.prerelease => vec!["edited", "released"],
504 (Some(_), Some(_)) => vec!["edited"],
505 }
506}
507
508/// The event kind for a release activity type from [`release_actions`].
509pub fn release_kind(action: &str) -> Option<&'static str> {
510 Some(match action {
511 "created" => "release.created",
512 "published" => "release.published",
513 "released" => "release.released",
514 "prereleased" => "release.prereleased",
515 "edited" => "release.edited",
516 "unpublished" => "release.unpublished",
517 "deleted" => "release.deleted",
518 _ => return None,
519 })
520}
521
522#[derive(Debug, Serialize)]
523#[serde(rename_all = "camelCase")]
The keeper reads Cloudflare as it really answers524pub struct SessionAppended {
525 pub pull_id: String,
526 pub repo_id: String,
527 pub number: u32,
528 pub count: u32,
529}
530
531/// An event as stored in the log and delivered to subscribers. `data` is
532/// left as JSON; each reader decodes the types it cares about.
533#[derive(Clone, Debug, Serialize, serde::Deserialize)]
534#[serde(rename_all = "camelCase")]
535pub struct Event {
536 /// Sorts by the time the event was published.
537 pub id: String,
538 #[serde(rename = "type")]
539 pub kind: String,
540 /// The service that published it.
541 pub source: String,
542 /// RFC 3339.
543 pub time: String,
544 /// The repo the event concerns.
545 pub repo_id: Option<String>,
546 /// The user or agent that caused it, if any.
547 pub actor: Option<String>,
548 pub data: serde_json::Value,
549}
550
551/// `publish`, as a publisher sends it. Returns nothing.
552#[derive(Debug, Serialize)]
553pub struct Publish<T: Serialize> {
554 pub events: Vec<NewEvent<T>>,
555}
556
557/// `publish`, as the events service reads it.
558#[derive(Debug, serde::Deserialize)]
559pub struct PublishArgs {
560 pub events: Vec<Published>,
561}
562
563/// A [`NewEvent`] of any type, as received.
564#[derive(Debug, serde::Deserialize)]
565#[serde(rename_all = "camelCase")]
566pub struct Published {
567 #[serde(rename = "type")]
568 pub kind: String,
569 pub source: String,
570 #[serde(default)]
571 pub repo_id: Option<String>,
572 #[serde(default)]
573 pub actor: Option<String>,
574 pub data: serde_json::Value,
575}
576
577/// `list`: events from the log, newest first. Returns `Vec<Event>`.
578#[derive(Debug, Default, Serialize, serde::Deserialize)]
579#[serde(rename_all = "camelCase")]
580pub struct ListArgs {
581 #[serde(default)]
582 pub repo_id: Option<String>,
583 /// Only these types; all types when empty.
584 #[serde(default)]
585 pub types: Vec<String>,
586 /// Only events older than this event id.
587 #[serde(default)]
588 pub before: Option<String>,
Merge leftovers: plan activity filtered in the query, pushes counted by account, ghost during the deletion window, profile time zones (identity 0039)589 /// Only events by this account id.
590 #[serde(default)]
591 pub actor: Option<String>,
592 /// Only events about these issues or pull requests: their `number`, or
593 /// the `issue` a comment, review or link is on. All when empty.
594 #[serde(default)]
595 pub numbers: Vec<u32>,
596 /// Only events at or after this RFC 3339 time.
597 #[serde(default)]
598 pub since: Option<String>,
The keeper reads Cloudflare as it really answers599 #[serde(default)]
600 pub limit: Option<u32>,
601}
602
Agents and memory, checks and conflicts, profiles, slug renames, custom domains603/// `workspace.renamed`: a workspace's slug changed from `from` to `to`.
604/// Every service that stores a slug moves its rows to the workspace's
605/// *current* slug (ask identity by `workspace_id`), so that a repeated or
606/// late delivery after a second rename still lands in the right place.
607#[derive(Clone, Debug, Serialize, serde::Deserialize)]
608#[serde(rename_all = "camelCase")]
609pub struct WorkspaceRenamed {
610 pub workspace_id: String,
611 pub from: String,
612 pub to: String,
613}
614
615impl WorkspaceRenamed {
616 /// The slugs whose rows move to `current`: the two this rename names,
617 /// minus `current` itself. Moving rows keyed by either converges on the
618 /// current slug whatever order renames are delivered in.
619 pub fn stale_slugs(&self, current: &str) -> Vec<String> {
620 let mut slugs: Vec<String> = Vec::new();
621 for slug in [&self.from, &self.to] {
622 if slug != current && !slugs.contains(slug) {
623 slugs.push(slug.clone());
624 }
625 }
626 slugs
627 }
628}
629
Search across all of g1t, Explore, and a command palette630/// `repo.updated`: a repository's description, topics or visibility
631/// changed. `visibility_changed` says whether it went public or private,
632/// which `repo.visibility_changed` also announces on its own.
633#[derive(Debug, Serialize, serde::Deserialize)]
634#[serde(rename_all = "camelCase")]
635pub struct RepoUpdated {
636 pub repo_id: String,
637 pub namespace: String,
638 pub name: String,
639 pub is_private: bool,
640 #[serde(default)]
641 pub visibility_changed: bool,
642}
643
644/// `repo.visibility_changed`: a repository went public or private.
645#[derive(Debug, Serialize, serde::Deserialize)]
646#[serde(rename_all = "camelCase")]
647pub struct RepoVisibilityChanged {
648 pub repo_id: String,
649 pub is_private: bool,
650}
651
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look652/// `repo.renamed`: a repository's name changed within its workspace,
653/// keeping its id and its git store key. Like `repo.transferred`, a path
654/// change: every service that keeps rows under the repository's path moves
655/// them to its *current* path (ask repos `path_by_id`), so a repeated or
656/// late delivery after a second rename or a transfer still lands in the
657/// right place. `g1t_kit::transfer::on_event` handles both.
658#[derive(Clone, Debug, Serialize, serde::Deserialize)]
Search across all of g1t, Explore, and a command palette659#[serde(rename_all = "camelCase")]
660pub struct RepoRenamed {
661 pub repo_id: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look662 /// The workspace it is in.
Search across all of g1t, Explore, and a command palette663 pub namespace: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look664 /// Its old name.
665 pub from: String,
666 /// Its new name.
667 pub to: String,
668}
669
670impl RepoRenamed {
671 /// The paths whose rows move to `current` (`namespace/name`): the two
672 /// this rename names, minus `current`.
673 pub fn stale_paths(&self, current: &str) -> Vec<String> {
674 let mut paths: Vec<String> = Vec::new();
675 for name in [&self.from, &self.to] {
676 let path = format!("{}/{name}", self.namespace);
677 if path != current && !paths.contains(&path) {
678 paths.push(path);
679 }
680 }
681 paths
682 }
683}
684
685/// `repo.deleted`: a repository was deleted. It is hidden everywhere and
686/// git refuses it, but it can be restored until `purge_after`, so services
687/// stop what runs for it (agents, workflows, deployments, indexing,
688/// webhook deliveries) and hide it, and keep what they hold until
689/// `repo.purged`. `repo.restored` brings it back.
690#[derive(Clone, Debug, Serialize, serde::Deserialize)]
691#[serde(rename_all = "camelCase")]
692pub struct RepoDeleted {
693 pub repo_id: String,
694 #[serde(default)]
695 pub namespace: String,
696 #[serde(default)]
Search across all of g1t, Explore, and a command palette697 pub name: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look698 #[serde(default)]
699 pub is_private: bool,
700 /// RFC 3339: when it is purged unless restored first.
701 #[serde(default)]
702 pub purge_after: String,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member703 /// It went with its workspace (`workspace.deleting`). Services that
704 /// handle the workspace as a whole (deployments pauses its apps rather
705 /// than taking them down) leave this one to that.
706 #[serde(default)]
707 pub with_workspace: bool,
Search across all of g1t, Explore, and a command palette708}
709
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look710/// `repo.restored`: a deleted repository is back, at its path, as it was.
711/// Services start again what `repo.deleted` stopped: index it, deploy its
712/// production, show it.
713#[derive(Clone, Debug, Serialize, serde::Deserialize)]
714#[serde(rename_all = "camelCase")]
715pub struct RepoRestored {
716 pub repo_id: String,
717 pub namespace: String,
718 pub name: String,
719 pub is_private: bool,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member720 /// It came back with its workspace (`workspace.restored`).
721 #[serde(default)]
722 pub with_workspace: bool,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look723}
724
725/// `repo.purged`: a deleted repository is gone for good, its git data
726/// with it. Services drop every row they keep for it by `repo_id`, except
727/// history that belongs to its workspace: ledgers, invoices and the audit
728/// log. Its path is free for a new repository.
729#[derive(Clone, Debug, Serialize, serde::Deserialize)]
730#[serde(rename_all = "camelCase")]
731pub struct RepoPurged {
732 pub repo_id: String,
733 pub namespace: String,
734 pub name: String,
735}
736
737/// `repo.archived` and `repo.unarchived`: a repository became read-only,
738/// or writable again. While archived, pushes are refused, issues and pull
739/// requests are locked, and agents and workflows do not run for it; its
740/// deployments keep serving.
741#[derive(Clone, Debug, Serialize, serde::Deserialize)]
742#[serde(rename_all = "camelCase")]
743pub struct RepoArchived {
744 pub repo_id: String,
745 pub namespace: String,
746 pub name: String,
747 pub archived: bool,
748}
749
750/// `repo.default_branch_changed`: the branch everything lands on is now
751/// `to`. `renamed` says whether `from` was renamed to `to` (open pull
752/// requests into it now target `to`) rather than another branch chosen.
753#[derive(Clone, Debug, Serialize, serde::Deserialize)]
754#[serde(rename_all = "camelCase")]
755pub struct RepoDefaultBranchChanged {
756 pub repo_id: String,
757 pub from: String,
758 pub to: String,
759 #[serde(default)]
760 pub renamed: bool,
761}
762
763/// `branch.renamed`: a branch was renamed. Pull requests from or into
764/// `from` follow it to `to`, and web addresses that name `from` redirect.
765#[derive(Clone, Debug, Serialize, serde::Deserialize)]
766#[serde(rename_all = "camelCase")]
767pub struct BranchRenamed {
768 pub repo_id: String,
769 pub from: String,
770 pub to: String,
771 /// Whether it is the default branch.
772 #[serde(default)]
773 pub default_branch: bool,
774}
775
776/// `repo.transferred`: a repository moved from one workspace to another,
777/// keeping its id and its name. Every service that keeps a repository
778/// under its path (`namespace/name`) or its workspace's slug moves those
779/// rows to the repository's *current* path (ask repos `path_by_id`), so a
780/// repeated or late delivery after a second transfer still lands in the
781/// right place. What was charged or recorded before the transfer stays
782/// with the workspace it happened in.
783#[derive(Clone, Debug, Serialize, serde::Deserialize)]
Search across all of g1t, Explore, and a command palette784#[serde(rename_all = "camelCase")]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look785pub struct RepoTransferred {
Search across all of g1t, Explore, and a command palette786 pub repo_id: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look787 pub name: String,
788 /// The workspace it left.
789 pub from: String,
790 /// The workspace it went to.
791 pub to: String,
Search across all of g1t, Explore, and a command palette792}
793
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look794impl RepoTransferred {
795 /// The paths whose rows move to `current` (`namespace/name`): the two
796 /// this transfer names, minus `current`. Moving rows keyed by either
797 /// converges whatever order transfers are delivered in.
798 pub fn stale_paths(&self, current: &str) -> Vec<String> {
799 let mut paths: Vec<String> = Vec::new();
800 for namespace in [&self.from, &self.to] {
801 let path = format!("{namespace}/{}", self.name);
802 if path != current && !paths.contains(&path) {
803 paths.push(path);
804 }
805 }
806 paths
807 }
808}
809
810/// `workspace.deleted`: a workspace is gone. Services drop what they keep
811/// for it alone (its webhooks, integrations, secrets, memory, guardrails,
812/// agent queue) and keep what is history: ledgers, invoices and the audit
813/// log stay under its slug, which is never given to another workspace.
814#[derive(Clone, Debug, Serialize, serde::Deserialize)]
815#[serde(rename_all = "camelCase")]
816pub struct WorkspaceDeleted {
817 pub workspace_id: String,
818 pub slug: String,
819}
820
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member821/// `workspace.deleting`: an owner deleted a workspace, and it can be
822/// restored by g1t's staff until `purge_after`. Nobody can reach it in the
823/// meantime. Services hide what they keep for it and stop what runs for it,
824/// keeping their rows: repos deletes its repositories softly (each with a
825/// `repo.deleted` whose `with_workspace` is set), deployments pauses its
826/// apps, search drops it from results. `workspace.restored` undoes exactly
827/// that; once `purge_after` passes, `workspace.deleted` follows and
828/// services purge as for any deleted workspace.
829#[derive(Clone, Debug, Serialize, serde::Deserialize)]
830#[serde(rename_all = "camelCase")]
831pub struct WorkspaceDeleting {
832 pub workspace_id: String,
833 pub slug: String,
834 /// The username of the owner who deleted it.
835 pub by: String,
836 /// RFC 3339: when it is purged unless restored first.
837 pub purge_after: String,
838}
839
840/// `workspace.restored`: staff brought a deleted workspace back, with its
841/// members and tokens. Services undo what they did on `workspace.deleting`,
842/// and only that: a repository deleted on its own before stays deleted.
843#[derive(Clone, Debug, Serialize, serde::Deserialize)]
844#[serde(rename_all = "camelCase")]
845pub struct WorkspaceRestored {
846 pub workspace_id: String,
847 pub slug: String,
848}
849
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)850/// `user.deleting`: an account was deleted, by the person or by g1t's
851/// staff, and can be restored by staff until `purge_after`. Its sessions,
852/// tokens and keys have ended and it has left every workspace; services
853/// stop what they do for it (search drops its profile, nothing more is
854/// sent to it) and keep their rows. `user.restored` undoes that; once
855/// `purge_after` passes, `user.deleted` follows.
856#[derive(Clone, Debug, Serialize, serde::Deserialize)]
857#[serde(rename_all = "camelCase")]
858pub struct UserDeleting {
859 pub user_id: String,
860 pub username: String,
861 /// Whether g1t's staff deleted it rather than the person.
862 #[serde(default)]
863 pub by_staff: bool,
864 /// RFC 3339: when it is purged unless restored first.
865 pub purge_after: String,
866}
867
868/// `user.restored`: staff brought a deleted account back. It signs in
869/// again with its password; its old sessions and tokens stay ended.
870/// Services undo what they did on `user.deleting`.
871#[derive(Clone, Debug, Serialize, serde::Deserialize)]
872#[serde(rename_all = "camelCase")]
873pub struct UserRestored {
874 pub user_id: String,
875 pub username: String,
876}
877
878/// `user.deleted`: an account is gone for good. Services drop what they
879/// keep for it alone (its inbox, subscriptions, settings) and show what it
880/// wrote as `ghost` (`account_deletion::GHOST_USERNAME`, id
881/// `account_deletion::GHOST_ID`). Ledgers, invoices and audit logs keep
882/// its username. The username is never given to anyone again.
883#[derive(Clone, Debug, Serialize, serde::Deserialize)]
884#[serde(rename_all = "camelCase")]
885pub struct UserDeleted {
886 pub user_id: String,
887 pub username: String,
888}
889
Search across all of g1t, Explore, and a command palette890/// `user.updated`: an account was made, or changed what its profile shows
891/// (name, bio, avatar). Nothing private: ask identity for the profile.
892#[derive(Debug, Serialize, serde::Deserialize)]
893#[serde(rename_all = "camelCase")]
894pub struct UserUpdated {
895 pub username: String,
896}
897
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look898/// `user.email_added`, `user.email_verified`, `user.email_removed` and
899/// `user.primary_email_changed`: an account's addresses changed. Never the
900/// address itself; ask identity, as the person, for that.
901#[derive(Debug, Serialize, serde::Deserialize)]
902#[serde(rename_all = "camelCase")]
903pub struct UserEmailChanged {
904 pub user_id: String,
905 /// Whether g1t staff made the change.
906 #[serde(default)]
907 pub by_staff: bool,
908}
909
Search across all of g1t, Explore, and a command palette910/// `workspace.updated`: a workspace was made, or its name, description or
911/// icon changed. Ask identity for it by slug.
912#[derive(Debug, Serialize, serde::Deserialize)]
913#[serde(rename_all = "camelCase")]
914pub struct WorkspaceUpdated {
915 pub workspace_id: String,
916 pub slug: String,
917}
918
@g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so.919/// How someone came to be a member of a workspace (`workspace.member_joined`).
920#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
921#[serde(rename_all = "snake_case")]
922pub enum JoinedHow {
923 /// They accepted a workspace invitation sent to them by name.
924 Invitation,
925 /// They made the workspace, and are its first owner.
926 Created,
927 /// They used an invite code or link that named the workspace.
928 Joined,
929}
930
931/// `workspace.member_joined`: a person became a member of a workspace,
932/// published by identity as the membership is written. `how` says by
933/// which door; the workspace's creator is `created`, so a listener that
934/// greets new members can leave them out (the agents service's @g1t
935/// welcomes everyone else, once). Never published for an agent or a
936/// token: only a person joins.
937#[derive(Clone, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
938#[serde(rename_all = "camelCase")]
939pub struct MemberJoined {
940 pub workspace_id: String,
941 /// The workspace's slug.
942 pub workspace: String,
943 pub user_id: String,
944 pub username: String,
945 pub role: crate::Role,
946 pub how: JoinedHow,
947}
948
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look949/// `invite.created`: someone (or staff) made an invite. Never the code or
950/// the address it is for.
951#[derive(Debug, Serialize, serde::Deserialize)]
952#[serde(rename_all = "camelCase")]
953pub struct InviteCreated {
954 pub invite_id: String,
955 /// The account that made it; null when staff did.
956 pub inviter_id: Option<String>,
957 /// The workspace it joins.
958 pub workspace_id: Option<String>,
959 /// Whether it is bound to one email address.
960 pub bound: bool,
961}
962
963/// `invite.redeemed`: an invite was used, by a new account or by an
964/// existing one joining a workspace.
965#[derive(Debug, Serialize, serde::Deserialize)]
966#[serde(rename_all = "camelCase")]
967pub struct InviteRedeemed {
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)968 /// The invite's id, or a shared invite link's (`sinv_…`) when one made
969 /// the account.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look970 pub invite_id: String,
971 pub user_id: String,
972 pub inviter_id: Option<String>,
973 pub workspace_id: Option<String>,
974 /// Whether it made the account.
975 pub created_account: bool,
976}
977
978/// `waitlist.requested`: someone asked for access. Ask identity's staff
979/// methods for the entry; the address is not in the event.
980#[derive(Debug, Serialize, serde::Deserialize)]
981#[serde(rename_all = "camelCase")]
982pub struct WaitlistRequested {
983 pub entry_id: String,
984}
985
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member986/// The payload of `package.published`, `package.version_deleted`,
987/// `package.deleted` and `package.visibility_changed`; each uses the fields
988/// that apply to it. `repo_id` is the repository the package is linked to.
989#[derive(Clone, Debug, Default, Serialize)]
990#[serde(rename_all = "camelCase")]
991pub struct PackageEvent {
992 pub package_id: String,
993 pub workspace: String,
994 pub ecosystem: String,
995 pub name: String,
996 pub repo_id: Option<String>,
997 /// The version published or deleted: for a container image, its
998 /// manifest's digest.
999 #[serde(skip_serializing_if = "Option::is_none")]
1000 pub version: Option<String>,
1001 #[serde(skip_serializing_if = "Option::is_none")]
1002 pub digest: Option<String>,
1003 #[serde(skip_serializing_if = "Option::is_none")]
1004 pub size: Option<u64>,
1005 /// On publish: the tags that now point to the version.
1006 #[serde(skip_serializing_if = "Option::is_none")]
1007 pub tags: Option<Vec<String>>,
1008 /// On `package.visibility_changed`: `public` or `private`.
1009 #[serde(skip_serializing_if = "Option::is_none")]
1010 pub visibility: Option<String>,
1011}
1012
The keeper reads Cloudflare as it really answers1013/// `queue.changed`: a repository's merge queue gained, lost or settled an
1014/// entry, so the next batch may be ready to test.
1015#[derive(Debug, Serialize)]
1016#[serde(rename_all = "camelCase")]
1017pub struct QueueChanged {
1018 pub repo_id: String,
1019}
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1020
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.1021/// The `doc.page.*` types the artifacts service (services/artifacts, TypeScript)
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store1022/// publishes, with no `repoId` on the event: a page may be in a private
1023/// space, so it never reaches a repository's timeline or webhooks.
1024pub const DOC_PAGE_EVENTS: [&str; 4] = ["doc.page.created", "doc.page.updated", "doc.page.archived", "doc.page.stale"];
1025
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet1026/// The `folio.*` types (Artifacts mode), with their payloads, live in
1027/// [`crate::folios`]; nothing publishes or subscribes to them yet.
1028pub use crate::folios::FOLIO_EVENTS;
1029
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store1030/// What every `doc.page.*` event carries (`DocPageEventData` in
1031/// events.ts). `doc.page.updated` adds `versionId`, `kind` and `authors`;
1032/// `doc.page.stale` adds `repoId`, `repo`, `commit`, `pull`, `paths` and
1033/// `owners` ([`DocPageStale`]).
1034#[derive(Clone, Debug, Serialize, serde::Deserialize)]
1035#[serde(rename_all = "camelCase")]
1036pub struct DocPageEvent {
1037 pub workspace: String,
1038 pub workspace_id: String,
1039 pub page_id: String,
1040 pub space_id: String,
1041 pub title: String,
1042 /// The page's address on the site.
1043 pub path: String,
1044}
1045
1046/// `doc.page.stale`: code a page cites changed.
1047#[derive(Clone, Debug, Serialize, serde::Deserialize)]
1048#[serde(rename_all = "camelCase")]
1049pub struct DocPageStale {
1050 #[serde(flatten)]
1051 pub page: DocPageEvent,
1052 pub repo_id: String,
1053 /// `owner/name`.
1054 pub repo: String,
1055 pub commit: String,
1056 /// The merged pull request's number, when a pull request made the change.
1057 pub pull: Option<u32>,
1058 pub paths: Vec<String>,
1059 /// Member keys: `user:<id>`, `agent:<id>`.
1060 pub owners: Vec<String>,
1061}
1062
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.1063/// `activity_digest`: what happened in some repositories, and to a
1064/// workspace's artifacts, over a span, counted. Returns [`ActivityDigest`].
1065/// Home reads it for what people and agents did since you were last there.
1066/// Fields are snake_case, as every service's arguments are.
1067///
1068/// The span is `[from, until)` in RFC 3339. Event ids sort by time, so the
1069/// service reads each repository's events of each type it counts as one
1070/// range of the `(repo_id, type, id)` index, from `id_floor(from)` to
1071/// `id_floor(until)`; a workspace's folio events (which carry no
1072/// repository) are the same range under a null repository, narrowed to
1073/// the workspace. That is indexed range scans per repository and type, and
1074/// it suits a workspace of tens of repositories. When a workspace has
1075/// hundreds, the next step is a daily rollup per workspace that this
1076/// method reads instead of the log; it is not built yet.
1077#[derive(Debug, Default, Serialize, serde::Deserialize)]
1078pub struct ActivityDigestArgs {
1079 /// The repositories to count, by id. The caller has checked the
1080 /// viewer may read each. At most `MAX_DIGEST_REPOS` are read.
1081 #[serde(default)]
1082 pub repo_ids: Vec<String>,
1083 /// The workspace's slug, for its `folio.*` events; none leaves
1084 /// artifacts out.
1085 #[serde(default)]
1086 pub workspace: Option<String>,
1087 /// The start of the span, inclusive. RFC 3339.
1088 pub from: String,
1089 /// The end of the span, exclusive. RFC 3339.
1090 pub until: String,
1091}
1092
1093/// The most repositories one digest reads.
1094pub const MAX_DIGEST_REPOS: usize = 50;
1095/// The most events one digest reads of repositories, and of folios; past
1096/// either the digest says it is not complete.
1097pub const MAX_DIGEST_EVENTS: u32 = 5000;
1098/// The most releases, packages and edited folios a digest lists.
1099pub const MAX_DIGEST_LISTED: usize = 50;
1100
1101/// What `activity_digest` returns.
1102#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq)]
1103pub struct ActivityDigest {
1104 pub from: String,
1105 pub until: String,
1106 /// One per repository that had any event counted; none for a quiet one.
1107 pub repos: Vec<RepoDigest>,
1108 /// The workspace's artifacts, when a workspace was named.
1109 pub folios: Option<FolioDigest>,
1110 /// False when more events fell in the span than were read, or more
1111 /// repositories were named than are read, so counts are low.
1112 pub complete: bool,
1113}
1114
1115/// How many times one actor did something. The actor is a member key:
1116/// `user:<id>` for a person (or g1t, `user:usr_g1t_agent`), `agent:<id>`
1117/// for one of the workspace's agents acting as itself, or empty when the
1118/// event named nobody (a push copied in by a mirror).
1119#[derive(Debug, Clone, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1120pub struct ActorCount {
1121 pub actor: String,
1122 pub count: u32,
1123}
1124
1125/// Pushes by one actor, with the commits they brought.
1126#[derive(Debug, Clone, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1127pub struct PushesBy {
1128 pub actor: String,
1129 pub count: u32,
1130 pub commits: u32,
1131}
1132
1133/// Pushes to a repository's branches (`git.push` events for `refs/heads/`;
1134/// tags are not pushes here). A push whose commits were not counted
1135/// counts as one commit.
1136#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1137pub struct PushDigest {
1138 pub count: u32,
1139 pub commits: u32,
1140 /// The branches pushed to, by name, sorted.
1141 pub branches: Vec<String>,
1142 /// Most pushes first.
1143 pub by: Vec<PushesBy>,
1144 /// Of those, the pushes straight to the default branch: what landed
1145 /// without a pull request.
1146 pub default_branch: DefaultBranchPushes,
1147}
1148
1149#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1150pub struct DefaultBranchPushes {
1151 pub count: u32,
1152 pub commits: u32,
1153 pub by: Vec<PushesBy>,
1154 /// When the newest of them was. RFC 3339.
1155 pub last_at: Option<String>,
1156}
1157
1158/// Pull requests opened, merged and closed without merging, each by who did it.
1159#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1160pub struct PullDigest {
1161 pub opened: Vec<ActorCount>,
1162 pub merged: Vec<ActorCount>,
1163 pub closed: Vec<ActorCount>,
1164}
1165
1166/// Issues opened and closed, each by who did it.
1167#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1168pub struct IssueDigest {
1169 pub opened: Vec<ActorCount>,
1170 pub closed: Vec<ActorCount>,
1171}
1172
1173/// Builds that finished, by who set them off, and how many of the
1174/// successes were production.
1175#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1176pub struct DeploymentDigest {
1177 pub succeeded: Vec<ActorCount>,
1178 pub failed: Vec<ActorCount>,
1179 pub production: u32,
1180}
1181
1182/// A release published in the span.
1183#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1184pub struct ReleaseDigest {
1185 pub tag: String,
1186 pub name: Option<String>,
1187 pub actor: String,
1188 /// RFC 3339.
1189 pub at: String,
1190}
1191
1192/// A package version published in the span.
1193#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1194pub struct PackageDigest {
1195 pub ecosystem: String,
1196 pub name: String,
1197 pub version: String,
1198 pub actor: String,
1199 /// RFC 3339.
1200 pub at: String,
1201}
1202
1203/// One repository's span.
1204#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq)]
1205pub struct RepoDigest {
1206 pub repo_id: String,
1207 pub pushes: PushDigest,
1208 pub pulls: PullDigest,
1209 pub issues: IssueDigest,
1210 /// Reviews: `review.completed` (g1t's), and a comment with a verdict (a person's).
1211 pub reviews: Vec<ActorCount>,
1212 /// Comments without a verdict; one by an agent as itself is the agent's.
1213 pub comments: Vec<ActorCount>,
1214 pub deployments: DeploymentDigest,
1215 /// Newest first, at most `MAX_DIGEST_LISTED`.
1216 pub releases: Vec<ReleaseDigest>,
1217 /// Newest first, at most `MAX_DIGEST_LISTED`.
1218 pub packages: Vec<PackageDigest>,
1219}
1220
1221/// One folio (an artifact: a doc, slides, a design, a dashboard) whose
1222/// content changed, with everyone whose changes are in it, as member
1223/// keys. Never its title.
1224#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1225pub struct FolioEdited {
1226 pub folio_id: String,
1227 pub kind: String,
1228 pub authors: Vec<String>,
1229}
1230
1231/// A workspace's artifacts over the span: `folio.created` by who made
1232/// them, and the folios whose content changed (`folio.updated`).
1233#[derive(Debug, Default, Serialize, serde::Deserialize, PartialEq, Eq)]
1234pub struct FolioDigest {
1235 pub created: Vec<ActorCount>,
1236 /// Each folio once, at most `MAX_DIGEST_LISTED`.
1237 pub edited: Vec<FolioEdited>,
1238 /// How many folios changed, counting those past the cap.
1239 pub edited_count: u32,
1240}
1241
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1242#[cfg(test)]
1243mod tests {
1244 use super::*;
1245
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1246 #[test]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1247 fn comment_edits_and_deletes_say_what_it_was() {
1248 let edited = CommentEdited {
1249 comment_id: "cmt_1".into(),
1250 repo_id: "rep_1".into(),
1251 number: 7,
1252 pull_id: Some("pr_1".into()),
1253 changes: CommentChanges { body: ChangedFrom { from: "Before".into() } },
1254 };
1255 assert_eq!(
1256 serde_json::to_value(edited).unwrap(),
1257 serde_json::json!({
1258 "commentId": "cmt_1", "repoId": "rep_1", "number": 7, "pullId": "pr_1",
1259 "changes": { "body": { "from": "Before" } },
1260 })
1261 );
1262 let deleted = CommentDeleted {
1263 comment_id: "cmt_1".into(),
1264 repo_id: "rep_1".into(),
1265 number: 7,
1266 pull_id: None,
1267 comment: DeletedComment {
1268 id: "cmt_1".into(),
1269 body: "Gone".into(),
1270 author: crate::credentials::Principal { id: "usr_1".into(), username: "ana".into() },
1271 created_at: "2026-10-08T00:00:00Z".into(),
1272 path: None,
1273 line: None,
1274 },
1275 };
1276 assert_eq!(
1277 serde_json::to_value(deleted).unwrap(),
1278 serde_json::json!({
1279 "commentId": "cmt_1", "repoId": "rep_1", "number": 7,
1280 "comment": {
1281 "id": "cmt_1", "body": "Gone", "author": { "id": "usr_1", "username": "ana" },
1282 "createdAt": "2026-10-08T00:00:00Z", "path": null, "line": null,
1283 },
1284 })
1285 );
1286 }
1287
1288 #[test]
1289 fn a_release_change_is_githubs_activity_types() {
1290 let state = |draft, prerelease| Some(ReleaseState { draft, prerelease });
1291 assert_eq!(release_actions(None, state(true, false)), ["created"]);
1292 assert_eq!(release_actions(None, state(false, false)), ["created", "published", "released"]);
1293 assert_eq!(release_actions(None, state(false, true)), ["created", "published", "prereleased"]);
1294 assert_eq!(release_actions(state(true, true), state(false, true)), ["published", "prereleased"]);
1295 assert_eq!(release_actions(state(false, false), state(true, false)), ["unpublished"]);
1296 assert_eq!(release_actions(state(false, true), state(false, false)), ["edited", "released"]);
1297 assert_eq!(release_actions(state(false, false), state(false, true)), ["edited"]);
1298 assert_eq!(release_actions(state(true, false), state(true, false)), ["edited"]);
1299 assert_eq!(release_actions(state(false, false), None), ["deleted"]);
1300 for action in ["created", "published", "released", "prereleased", "edited", "unpublished", "deleted"] {
1301 assert_eq!(release_kind(action).map(|kind| kind.trim_start_matches("release.")), Some(action));
1302 }
1303 assert_eq!(release_kind("archived"), None);
1304 }
1305
1306 #[test]
Merge branch 'worktree-agent-a3abfcce648e87dca'1307 fn what_a_job_token_did_is_marked_and_carried_on() {
1308 let mut actor = crate::User { id: "wsp_1".into(), username: "acme".into(), ..crate::User::default() };
1309 let plain = marked(serde_json::json!({ "number": 4 }), Some(&actor));
1310 assert_eq!(caused_by_job(&plain), None);
1311 actor.token = Some(Box::new(crate::scopes::TokenAccess {
1312 job: Some(crate::scopes::JobToken { run_id: "run_9".into(), job_id: "job_1".into(), pull_requests: false }),
1313 ..Default::default()
1314 }));
1315 let by_job = marked(serde_json::json!({ "number": 4 }), Some(&actor));
1316 assert_eq!(caused_by_job(&by_job), Some("run_9"));
1317 assert_eq!(by_job["number"], 4);
1318 // A push by the job's token, and the pull request it moves.
1319 let push = GitPush {
1320 repo_id: "rep_1".into(),
1321 git_ref: "refs/heads/fix".into(),
1322 before: Some("a".into()),
1323 after: "b".into(),
1324 default_branch: false,
1325 unscanned: false,
1326 caused_by_job: job_run_of(&actor).map(str::to_owned),
Merge branch 'mirroring' into artifacts-mode1327 mirrored: false,
1328 mirror: None,
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.1329 commits: Some(3),
Merge branch 'worktree-agent-a3abfcce648e87dca'1330 };
1331 let push = serde_json::to_value(push).unwrap();
1332 assert_eq!(caused_by_job(&push), Some("run_9"));
1333 assert_eq!(caused_by_job(&carried(serde_json::json!({ "number": 4 }), &push)), Some("run_9"));
1334 assert_eq!(caused_by_job(&carried(serde_json::json!({ "number": 4 }), &serde_json::json!({}))), None);
1335 }
1336
1337 #[test]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1338 fn a_push_says_it_was_unscanned_only_when_it_was() {
1339 let push = |unscanned| GitPush {
1340 repo_id: "rep_1".into(),
1341 git_ref: "refs/heads/import".into(),
1342 before: None,
1343 after: "abc".into(),
1344 default_branch: false,
1345 unscanned,
Merge branch 'worktree-agent-a3abfcce648e87dca'1346 caused_by_job: None,
Merge branch 'mirroring' into artifacts-mode1347 mirrored: false,
1348 mirror: None,
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.1349 commits: None,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1350 };
1351 let quiet = serde_json::to_value(push(false)).unwrap();
1352 assert!(quiet.get("unscanned").is_none());
1353 let flagged = serde_json::to_value(push(true)).unwrap();
1354 assert_eq!(flagged["unscanned"], true);
1355 assert_eq!(flagged["ref"], "refs/heads/import");
1356 }
1357
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1358 fn renamed(from: &str, to: &str) -> WorkspaceRenamed {
1359 WorkspaceRenamed {
1360 workspace_id: "wsp_1".into(),
1361 from: from.into(),
1362 to: to.into(),
1363 }
1364 }
1365
1366 #[test]
1367 fn stale_slugs_leave_out_the_current_one() {
1368 assert_eq!(renamed("a", "b").stale_slugs("b"), vec!["a"]);
1369 // Delivered after a second rename, b → c: both move to c.
1370 assert_eq!(renamed("a", "b").stale_slugs("c"), vec!["a", "b"]);
1371 // Renamed back: a → b → a.
1372 assert_eq!(renamed("a", "b").stale_slugs("a"), vec!["b"]);
1373 }
1374
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1375 fn transferred(from: &str, to: &str) -> RepoTransferred {
1376 RepoTransferred {
1377 repo_id: "rep_1".into(),
1378 name: "rocket".into(),
1379 from: from.into(),
1380 to: to.into(),
1381 }
1382 }
1383
1384 #[test]
1385 fn stale_paths_leave_out_the_current_one() {
1386 assert_eq!(transferred("a", "b").stale_paths("b/rocket"), vec!["a/rocket"]);
1387 // Delivered after a second transfer, b → c: both move to c.
1388 assert_eq!(
1389 transferred("a", "b").stale_paths("c/rocket"),
1390 vec!["a/rocket", "b/rocket"]
1391 );
1392 // Transferred back: a → b → a.
1393 assert_eq!(transferred("a", "b").stale_paths("a/rocket"), vec!["b/rocket"]);
1394 }
1395
1396 #[test]
1397 fn a_transfer_reads_as_published() {
1398 let data = serde_json::json!({ "repoId": "rep_1", "name": "rocket", "from": "a", "to": "b" });
1399 let event: RepoTransferred = serde_json::from_value(data).unwrap();
1400 assert_eq!((event.from.as_str(), event.to.as_str()), ("a", "b"));
1401 }
1402
1403 #[test]
1404 fn a_rename_names_both_paths_in_its_workspace() {
1405 let renamed = RepoRenamed {
1406 repo_id: "rep_1".into(),
1407 namespace: "acme".into(),
1408 from: "old".into(),
1409 to: "new".into(),
1410 };
1411 assert_eq!(renamed.stale_paths("acme/new"), vec!["acme/old"]);
1412 // Delivered after a transfer: both names in acme move.
1413 assert_eq!(renamed.stale_paths("flagon/new"), vec!["acme/old", "acme/new"]);
1414 }
1415
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1416 #[test]
@g1t welcomes you when you join a workspace, and that is the one introduction any agent makes. Identity now says when a person becomes a member, workspace.member_joined, by invitation, by an invite code, or by creating the workspace, and webhooks can hear it; the agents service hears it too, and for anyone who joined, never the creator, the workspace's @g1t opens your direct message and says in its own voice who it is, what to ask it, how agents answer, and where things are, once per person per workspace and charged to @g1t like any reply, with a fixed welcome when no model can be used. The agents, workspaces, authentication and webhooks guides say so.1417 fn a_member_joining_says_by_which_door() {
1418 let joined = MemberJoined {
1419 workspace_id: "wsp_1".into(),
1420 workspace: "acme".into(),
1421 user_id: "usr_ada".into(),
1422 username: "ada".into(),
1423 role: crate::Role::Member,
1424 how: JoinedHow::Invitation,
1425 };
1426 let data = serde_json::to_value(&joined).unwrap();
1427 assert_eq!(
1428 data,
1429 serde_json::json!({
1430 "workspaceId": "wsp_1",
1431 "workspace": "acme",
1432 "userId": "usr_ada",
1433 "username": "ada",
1434 "role": "member",
1435 "how": "invitation",
1436 })
1437 );
1438 let back: MemberJoined = serde_json::from_value(data).unwrap();
1439 assert_eq!(back, joined);
1440 assert_eq!(serde_json::to_value(JoinedHow::Created).unwrap(), "created");
1441 assert_eq!(serde_json::to_value(JoinedHow::Joined).unwrap(), "joined");
1442 }
1443
1444 #[test]
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1445 fn reads_the_published_payload() {
1446 let data = serde_json::json!({ "workspaceId": "wsp_1", "from": "a", "to": "b" });
1447 let event: WorkspaceRenamed = serde_json::from_value(data).unwrap();
1448 assert_eq!((event.from.as_str(), event.to.as_str()), ("a", "b"));
1449 }
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store1450
1451 #[test]
1452 fn a_stale_page_reads_as_published() {
1453 let data = serde_json::json!({
1454 "workspace": "acme", "workspaceId": "wsp_1", "pageId": "pag_1", "spaceId": "spc_1",
1455 "title": "Exports", "path": "/acme/-/docs/general/exports-pag_1",
1456 "repoId": "rep_1", "repo": "acme/web", "commit": "abc", "pull": 431,
1457 "paths": ["src/export.ts"], "owners": ["user:usr_1"]
1458 });
1459 let event: DocPageStale = serde_json::from_value(data).unwrap();
1460 assert_eq!((event.page.page_id.as_str(), event.pull), ("pag_1", Some(431)));
1461 assert!(DOC_PAGE_EVENTS.contains(&"doc.page.stale"));
1462 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1463}

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