Skip to content
360 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.

Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails1//! Which events each subscribing service's queue is sent.
2//!
3//! The events service passes each batch on to one queue per subscriber
4//! (`SUBSCRIBER_*` bindings). Each is sent only the types its consumer acts
5//! on, so an event costs a queue write for the services that read it, not
6//! for all of them. A consumer that starts acting on a new type adds it
7//! here in the same change, or it never hears of it; the tests below and
8//! beside each consumer check what they can.
9//!
10//! A pattern is an exact type (`git.push`), a family (`pull.*`, every type
11//! that starts `pull.`), or `*`, every type. A binding not named here is
12//! sent everything, so a new subscriber is never left out.
13
14/// What the shared helpers in `g1t_kit` act on (`rename`, `transfer`,
15/// `lifecycle`, `deleted`, `user_deleted`): rows that follow a workspace
16/// or repository, or go with it. Rare, so every subscriber is sent them.
17const LIFECYCLE: [&str; 8] = [
18 "workspace.renamed",
19 "workspace.deleted",
20 "repo.renamed",
21 "repo.transferred",
22 "repo.deleted",
23 "repo.restored",
24 "repo.purged",
25 "user.deleted",
26];
27
28/// Each subscriber's binding and the types it is sent beside [`LIFECYCLE`].
29pub const ROUTES: &[(&str, &[&str])] = &[
30 // Memory captured from merges and comments, mentions, pull request
31 // bases, runs stopped in archived repositories (work/src/lib.rs queue).
32 (
33 "SUBSCRIBER_WORK",
34 &[
35 "git.push",
36 "branch.renamed",
37 "repo.created",
38 "repo.archived",
39 "repo.default_branch_changed",
40 "pull.merged",
41 "comment.created",
42 ],
43 ),
44 // Pull requests g1t is seeing through, mentions, queued issues and the
45 // merge queue (runner/src/index.ts queue).
46 (
47 "SUBSCRIBER_RUNNER",
48 &[
49 "pull.opened",
50 "pull.ready",
51 "pull.updated",
52 "pull.mergeability",
53 "pull.mergecheck",
54 "pull.merge_requested",
55 "pull.merged",
56 "pull.closed",
57 "checks.completed",
58 "review.completed",
59 "queue.changed",
60 "comment.created",
61 "issue.opened",
62 "issue.updated",
63 "issue.closed",
64 "agent.asked",
65 "repo.archived",
66 ],
67 ),
Merge branch 'worktree-agent-a049c374e6f1c6be4' into claude/cool-newton-7nm8r968 // Push mirrors, write-back to linked trackers, and commit statuses
69 // reported to a GitHub mirror's commits (integrations/src lib.rs,
70 // remotes.rs on_status).
71 ("SUBSCRIBER_INTEGRATIONS", &["git.push", "issue.closed", "pull.opened", "status.created"]),
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails72 // A hook may ask for any type, or for every one.
73 ("SUBSCRIBER_WEBHOOKS", &["*"]),
74 // Every event a workflow can run on (g1t_actions::events::github_events),
75 // and runs stopped in archived repositories.
76 (
77 "SUBSCRIBER_ACTIONS",
78 &[
79 "git.push",
Merge branch 'worktree-agent-a8b78c62188153aa3' into claude/cool-newton-7nm8r980 "git.deleted",
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails81 "pull.*",
82 "issue.*",
83 "comment.*",
84 "release.*",
85 "deployment.created",
86 "deployment_status.created",
87 "review.completed",
88 "workflow.completed",
89 "repo.archived",
90 ],
91 ),
92 // Previews and production deploys (deployments/src/index.ts queue).
93 (
94 "SUBSCRIBER_DEPLOYMENTS",
95 &[
96 "git.push",
97 "branch.renamed",
98 "repo.default_branch_changed",
99 "pull.opened",
100 "pull.ready",
101 "pull.updated",
102 "pull.reopened",
103 "pull.closed",
104 "pull.merged",
105 "workspace.deleting",
106 "workspace.restored",
107 ],
108 ),
109 // Projects follow their repositories, and wake on activity
Merge branch 'worktree-agent-a5b0d9e2733e9e382'110 // (projects/src/index.ts, activity.ts). A production deployment
111 // reported from elsewhere asks for its screenshot.
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails112 (
113 "SUBSCRIBER_PROJECTS",
114 &[
115 "git.push",
116 "repo.*",
117 "issue.opened",
118 "issue.closed",
119 "issue.reopened",
120 "pull.opened",
121 "pull.updated",
122 "pull.merged",
123 "pull.closed",
124 "review.completed",
125 "comment.created",
126 "deployment.succeeded",
127 "deployment.failed",
Merge branch 'worktree-agent-a5b0d9e2733e9e382'128 "deployment_status.created",
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails129 "package.published",
130 "package.deleted",
131 "package.version_deleted",
132 "workspace.deleting",
133 "workspace.restored",
134 ],
135 ),
136 // Pull request working copies, workspaces' repositories, contributors
137 // shown as ghost (repos/src/lib.rs handle_event).
138 (
139 "SUBSCRIBER_REPOS",
140 &[
141 "pull.merged",
142 "pull.closed",
143 "pull.reopened",
144 "workspace.deleting",
145 "workspace.restored",
146 "user.deleting",
147 "user.restored",
148 ],
149 ),
150 // Usage rows that follow a renamed workspace or repository.
151 ("SUBSCRIBER_BILLING", &[]),
152 // Secret and dependency scans, and version update pull requests
153 // (security/src/lib.rs on_event).
154 (
155 "SUBSCRIBER_SECURITY",
156 &[
157 "git.push",
158 "pull.opened",
159 "pull.updated",
160 "pull.ready",
161 "pull.merged",
162 "pull.closed",
163 "checks.completed",
164 "comment.created",
165 "repo.created",
166 "repo.visibility_changed",
167 ],
168 ),
169 // What agents are told about a repository (context/src/index.ts).
170 (
171 "SUBSCRIBER_CONTEXT",
172 &[
173 "git.push",
174 "issue.opened",
175 "issue.updated",
176 "issue.closed",
177 "pull.ready",
178 "pull.merged",
179 "memory.changed",
180 ],
181 ),
182 // The site-wide search index (search/src/index.rs).
183 ("SUBSCRIBER_SEARCH", &["git.push", "repo.*", "issue.*", "pull.*", "user.*", "workspace.*"]),
184 // Composer packages read from repositories, and packages that follow
185 // their workspace, team or repository (packages/src/lib.rs on_event).
186 (
187 "SUBSCRIBER_PACKAGES",
188 &[
189 "git.push",
190 "repo.visibility_changed",
191 "workspace.deleting",
192 "workspace.restored",
193 "team.edited",
194 "team.deleted",
195 ],
196 ),
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store197 // Pages that cite code a merge or a push to the default branch
198 // changed become possibly out of date, and projects' docs folders shown
199 // in Docs are read again (docs/src/staleness.ts, repo-spaces.ts).
200 ("SUBSCRIBER_DOCS", &["git.push", "pull.merged"]),
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened201 // Agents' routines that run on events: a pull request ready for
202 // review or merged, checks or a deploy failing, an issue opened
@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.203 // (agents/src/triggers.ts); pushes, which skill libraries that
204 // follow a repository read again (agents/src/skill-library.ts); and
205 // a person joining a workspace, whom its @g1t welcomes
206 // (agents/src/welcome.ts).
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened207 (
208 "SUBSCRIBER_AGENTS",
209 &[
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.210 "git.push",
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened211 "pull.opened",
212 "pull.ready",
213 "pull.merged",
214 "checks.completed",
215 "issue.opened",
216 "deployment.failed",
@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.217 "workspace.member_joined",
Routines run when something happens: a pull request ready for review or merged, checks or a deploy failing, an issue opened218 ],
219 ),
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails220];
221
222/// Whether `pattern` (see the module's notes) matches the type `kind`.
223fn matches(pattern: &str, kind: &str) -> bool {
224 match pattern.strip_suffix('*') {
225 Some(prefix) => kind.starts_with(prefix),
226 None => pattern == kind,
227 }
228}
229
230/// Whether the subscriber behind `binding` is sent events of type `kind`.
231pub fn routed(binding: &str, kind: &str) -> bool {
232 match ROUTES.iter().find(|(name, _)| *name == binding) {
233 Some((_, patterns)) => {
234 LIFECYCLE.contains(&kind) || patterns.iter().any(|pattern| matches(pattern, kind))
235 }
236 None => true,
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 /// Types published on the bus that hooks are not offered.
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9245 const UNOFFERED: [&str; 30] = [
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails246 "pull.mergecheck",
247 "pull.mergeability",
248 "deployment.review_requested",
249 "memory.changed",
250 "abuse.flagged",
251 "invite.created",
252 "invite.redeemed",
253 "waitlist.requested",
254 "user.updated",
255 "user.deleting",
256 "user.restored",
257 "user.deleted",
258 "user.email_added",
259 "workspace.updated",
260 "workspace.renamed",
261 "workspace.deleting",
262 "workspace.restored",
263 "workspace.deleted",
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store264 // Docs pages: no repository, so no repository's hooks.
265 "doc.page.created",
266 "doc.page.updated",
267 "doc.page.archived",
268 "doc.page.stale",
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9269 "doc.space.deleted",
The artifacts plan records what Phase 1 decided, self-hosting and the deploy list name the g1t-folios index and the trash cron, and folio events are listed as published but never offered to webhooks.270 // Artifacts (folios): no repository either, and a folio may be private.
271 "folio.created",
272 "folio.updated",
273 "folio.trashed",
274 "folio.restored",
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9275 "folio.deleted",
The artifacts plan records what Phase 1 decided, self-hosting and the deploy list name the g1t-folios index and the trash cron, and folio events are listed as published but never offered to webhooks.276 "folio.shared",
277 "folio.stale",
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails278 ];
279
280 fn published(kind: &str) -> bool {
281 crate::webhooks::EVENT_TYPES.contains(&kind) || UNOFFERED.contains(&kind)
282 }
283
284 #[test]
285 fn every_exact_route_names_a_type_that_is_published() {
286 for (binding, patterns) in ROUTES {
287 for pattern in patterns.iter().chain(LIFECYCLE.iter()) {
288 if pattern.ends_with('*') {
289 let prefix = pattern.trim_end_matches('*');
290 assert!(
291 prefix.is_empty()
292 || crate::webhooks::EVENT_TYPES.iter().chain(UNOFFERED.iter()).any(|kind| kind.starts_with(prefix)),
293 "{binding}: {pattern} matches nothing"
294 );
295 } else {
296 assert!(published(pattern), "{binding}: {pattern} is not a type anything publishes");
297 }
298 }
299 }
300 }
301
302 #[test]
303 fn every_route_is_a_subscriber_binding_named_once() {
304 let mut names: Vec<&str> = ROUTES.iter().map(|(name, _)| *name).collect();
305 assert!(names.iter().all(|name| name.starts_with("SUBSCRIBER_")));
306 let count = names.len();
307 names.sort_unstable();
308 names.dedup();
309 assert_eq!(names.len(), count);
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store310 assert_eq!(count, 15);
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails311 }
312
313 #[test]
314 fn subscribers_hear_what_they_act_on_and_not_the_rest() {
315 assert!(routed("SUBSCRIBER_WEBHOOKS", "secret_scanning_alert.created"));
316 assert!(routed("SUBSCRIBER_WEBHOOKS", "anything.new"));
317 assert!(routed("SUBSCRIBER_ACTIONS", "pull.labeled"));
318 assert!(routed("SUBSCRIBER_ACTIONS", "release.published"));
319 assert!(!routed("SUBSCRIBER_ACTIONS", "check_run.completed"));
320 assert!(routed("SUBSCRIBER_RUNNER", "comment.created"));
321 assert!(!routed("SUBSCRIBER_RUNNER", "status.created"));
Merge branch 'worktree-agent-a049c374e6f1c6be4' into claude/cool-newton-7nm8r9322 assert!(routed("SUBSCRIBER_INTEGRATIONS", "status.created"));
323 assert!(!routed("SUBSCRIBER_INTEGRATIONS", "check_run.completed"));
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails324 assert!(routed("SUBSCRIBER_BILLING", "workspace.renamed"));
325 assert!(!routed("SUBSCRIBER_BILLING", "git.push"));
326 assert!(routed("SUBSCRIBER_SEARCH", "user.updated"));
327 assert!(routed("SUBSCRIBER_REPOS", "user.deleting"));
328 assert!(!routed("SUBSCRIBER_REPOS", "git.push"));
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store329 assert!(routed("SUBSCRIBER_DOCS", "pull.merged"));
330 assert!(!routed("SUBSCRIBER_DOCS", "pull.opened"));
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.331 assert!(routed("SUBSCRIBER_AGENTS", "git.push"));
@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.332 assert!(routed("SUBSCRIBER_AGENTS", "workspace.member_joined"));
Agents → Skills is the workspace's skill library in the open SKILL.md format: write a skill in g1t, import a SKILL.md or zip, link a repository whose .g1t/skills folders publish on every push, or save a finished session as a skill once a person reviews it; attach a skill to an agent, a team or the whole workspace, each attachment pinned to a version. Agents see each skill's name and when to use it, and read the rest with use_skill when a request needs it; a skill never adds a permission, and scripts wait for an agent's own computer. The agent skills guide says how.333 assert!(!routed("SUBSCRIBER_AGENTS", "comment.created"));
@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.334 assert!(!routed("SUBSCRIBER_AGENTS", "workspace.updated"));
Merge branch 'worktree-agent-a5b0d9e2733e9e382'335 assert!(routed("SUBSCRIBER_PROJECTS", "deployment_status.created"));
336 assert!(!routed("SUBSCRIBER_PROJECTS", "deployment.created"));
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails337 // Every subscriber follows what moves or removes a repository.
338 for (binding, _) in ROUTES {
339 for kind in LIFECYCLE {
340 assert!(routed(binding, kind), "{binding} misses {kind}");
341 }
342 }
343 }
344
345 #[test]
346 fn a_subscriber_not_in_the_table_hears_everything() {
347 assert!(routed("SUBSCRIBER_NEW", "git.push"));
348 assert!(routed("SUBSCRIBER_NEW", "status.created"));
349 }
350
351 #[test]
352 fn a_family_matches_only_its_own_types() {
353 assert!(matches("pull.*", "pull.opened"));
354 assert!(!matches("pull.*", "pulls.opened"));
355 assert!(!matches("pull.*", "pull"));
356 assert!(matches("*", "x"));
357 assert!(matches("git.push", "git.push"));
358 assert!(!matches("git.push", "git.pushed"));
359 }
360}

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