Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
1,629 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.

README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1//! REST: each route maps an HTTP request onto one operation.
2
3use serde_json::{Map, Value};
4
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb975use crate::about::AboutOp;
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.6use crate::run_artifacts::ArtifactsOp;
Merge branch 'worktree-agent-a3b9b1f730f55b5bf'7use crate::caches::CachesOp;
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.8use crate::archives::ArchiveOp;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca9use crate::deploy_keys::DeployKeysOp;
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9710use crate::deployments::DeploymentsOp;
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.11use crate::telemetry::TelemetryOp;
Merge packages: roles, Actions access, source label, soft delete, API12use crate::packages::PackagesOp;
Merge main into Artifacts Phase 213use crate::folios::FoliosOp;
Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae'14use crate::coordination::CoordinationOp;
Merge branch 'worktree-agent-a3abfcce648e87dca'15use crate::protection::ProtectionOp;
API and MCP for a workspace's personal access token rules, members' tokens and approvals16use crate::token_policy::TokenOp;
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise17use crate::operations::Op;
Merge checks: statuses and check runs on every commit18use crate::checks::ChecksOp;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge19use crate::rules::RulesOp;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar20use crate::security::SecurityOp;
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise21
22pub struct Route {
23 pub method: &'static str,
24 /// Segments starting with `:` are parameters.
25 pub path: &'static str,
26 pub op: Op,
27 /// Query parameters the route reads, as `(name in the URL, input name)`.
28 pub query: &'static [(&'static str, &'static str)],
29}
30
31const fn route(
32 method: &'static str,
33 path: &'static str,
34 op: Op,
35 query: &'static [(&'static str, &'static str)],
36) -> Route {
37 Route {
38 method,
39 path,
40 op,
41 query,
42 }
43}
44
45pub const ROUTES: &[Route] = &[
46 route("GET", "/user", Op::Whoami, &[]),
47 route("POST", "/workspaces", Op::CreateWorkspace, &[]),
Merge branch 'worktree-agent-ad7c6d88d93adc817'48 route("GET", "/workspaces/:workspace", Op::GetWorkspace, &[]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise49 route("DELETE", "/workspaces/:workspace", Op::DeleteWorkspace, &[]),
50 route("GET", "/user/emails", Op::ListEmails, &[]),
51 route("POST", "/user/emails", Op::AddEmail, &[]),
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)52 route("POST", "/user/emails/confirm", Op::ConfirmEmail, &[]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise53 route("DELETE", "/user/emails/:email", Op::RemoveEmail, &[]),
54 route("PATCH", "/user/email-settings", Op::UpdateEmailSettings, &[]),
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)55 // Invitations to workspaces waiting for your answer.
56 route("GET", "/user/invitations", Op::ListInvitations, &[]),
57 route("POST", "/user/invitations/:id/accept", Op::AcceptInvitation, &[]),
58 route("POST", "/user/invitations/:id/decline", Op::DeclineInvitation, &[]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise59 route("GET", "/workspaces/:workspace/invitations", Op::ListWorkspaceInvites, &[]),
API and MCP for a workspace's personal access token rules, members' tokens and approvals60 // A workspace's rules for personal access tokens, and its members' tokens.
61 route("GET", "/workspaces/:workspace/personal-access-token-policy", Op::Tokens(TokenOp::GetTokenPolicy), &[]),
62 route("PATCH", "/workspaces/:workspace/personal-access-token-policy", Op::Tokens(TokenOp::SetTokenPolicy), &[]),
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers63 route("GET", "/workspaces/:workspace/personal-access-tokens", Op::Tokens(TokenOp::ListMemberTokens), &[]),
API and MCP for a workspace's personal access token rules, members' tokens and approvals64 route("POST", "/workspaces/:workspace/personal-access-tokens/:id", Op::Tokens(TokenOp::RevokeMemberToken), &[]),
65 route("GET", "/workspaces/:workspace/personal-access-token-requests", Op::Tokens(TokenOp::ListTokenRequests), &[]),
66 route("POST", "/workspaces/:workspace/personal-access-token-requests/:id", Op::Tokens(TokenOp::ReviewTokenRequest), &[]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise67 route("POST", "/workspaces/:workspace/invitations", Op::InviteMember, &[]),
68 route("DELETE", "/workspaces/:workspace/invitations/:id", Op::RevokeWorkspaceInvite, &[]),
69 // Who has access. GitHub's addresses, but for adding someone, which
70 // takes an email address as well as a username.
71 route("GET", "/repos/:owner/:name/collaborators", Op::ListCollaborators, &[]),
72 route("POST", "/repos/:owner/:name/collaborators", Op::AddCollaborator, &[]),
73 route("PATCH", "/repos/:owner/:name/collaborators/:username", Op::UpdateCollaborator, &[]),
74 route("DELETE", "/repos/:owner/:name/collaborators/:username", Op::RemoveCollaborator, &[]),
75 route(
76 "GET",
77 "/repos/:owner/:name/collaborators/:username/permission",
78 Op::GetCollaboratorPermission,
79 &[],
80 ),
81 route("GET", "/repos/:owner/:name/invitations", Op::ListRepoInvitations, &[]),
82 route("DELETE", "/repos/:owner/:name/invitations/:id", Op::RevokeRepoInvitation, &[]),
83 route("GET", "/user/repository_invitations", Op::ListMyRepoInvitations, &[]),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca84 // Deploy keys, at GitHub's addresses.
85 route("GET", "/repos/:owner/:name/keys", Op::DeployKeys(DeployKeysOp::ListDeployKeys), &[]),
86 route("POST", "/repos/:owner/:name/keys", Op::DeployKeys(DeployKeysOp::CreateDeployKey), &[]),
87 route("GET", "/repos/:owner/:name/keys/:id", Op::DeployKeys(DeployKeysOp::GetDeployKey), &[]),
88 route("DELETE", "/repos/:owner/:name/keys/:id", Op::DeployKeys(DeployKeysOp::DeleteDeployKey), &[]),
API: notifications over REST and MCP, with notifications scopes89 // Your notifications: threads, marking them, and what you subscribe
90 // to and watch. GitHub's addresses, with g1t's saved and snoozed.
91 route("GET", "/notifications", Op::ListNotifications, &[("all", "all"), ("participating", "participating"), ("view", "view"), ("reason", "reason"), ("severity", "severity"), ("since", "since"), ("before", "before"), ("cursor", "cursor"), ("per_page", "per_page")]),
92 route("PUT", "/notifications", Op::MarkNotificationsRead, &[]),
93 route("GET", "/notifications/threads/:id", Op::GetNotificationThread, &[]),
94 route("PATCH", "/notifications/threads/:id", Op::MarkThreadRead, &[]),
95 route("DELETE", "/notifications/threads/:id", Op::MarkThreadDone, &[]),
96 route("PUT", "/notifications/threads/:id/saved", Op::SaveThread, &[]),
97 route("DELETE", "/notifications/threads/:id/saved", Op::SaveThread, &[]),
98 route("PUT", "/notifications/threads/:id/snooze", Op::SnoozeThread, &[]),
99 route("DELETE", "/notifications/threads/:id/snooze", Op::SnoozeThread, &[]),
100 route("GET", "/notifications/threads/:id/subscription", Op::GetThreadSubscription, &[]),
101 route("PUT", "/notifications/threads/:id/subscription", Op::SetThreadSubscription, &[]),
102 route("DELETE", "/notifications/threads/:id/subscription", Op::DeleteThreadSubscription, &[]),
103 route("GET", "/repos/:owner/:name/notifications", Op::ListNotifications, &[("all", "all"), ("participating", "participating"), ("view", "view"), ("reason", "reason"), ("severity", "severity"), ("since", "since"), ("before", "before"), ("cursor", "cursor"), ("per_page", "per_page")]),
104 route("PUT", "/repos/:owner/:name/notifications", Op::MarkNotificationsRead, &[]),
105 route("GET", "/repos/:owner/:name/subscription", Op::GetRepoSubscription, &[]),
106 route("PUT", "/repos/:owner/:name/subscription", Op::SetRepoSubscription, &[]),
107 route("DELETE", "/repos/:owner/:name/subscription", Op::DeleteRepoSubscription, &[]),
108 route("GET", "/repos/:owner/:name/issues/:number/subscription", Op::GetThreadSubscription, &[]),
109 route("PUT", "/repos/:owner/:name/issues/:number/subscription", Op::SetThreadSubscription, &[]),
110 route("DELETE", "/repos/:owner/:name/issues/:number/subscription", Op::DeleteThreadSubscription, &[]),
111 route("GET", "/user/subscriptions", Op::ListWatchedRepos, &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97112 // Stars: yours, and who starred a repository.
113 route("GET", "/user/starred", Op::About(AboutOp::ListStarred), &[]),
114 route("GET", "/user/starred/:owner/:name", Op::About(AboutOp::CheckStarred), &[]),
115 route("PUT", "/user/starred/:owner/:name", Op::About(AboutOp::Star), &[]),
116 route("DELETE", "/user/starred/:owner/:name", Op::About(AboutOp::Unstar), &[]),
117 route("GET", "/repos/:owner/:name/stargazers", Op::About(AboutOp::ListStargazers), &[("page", "page")]),
118 // What the default branch says about a repository, kept by commit.
119 route("GET", "/repos/:owner/:name/languages", Op::About(AboutOp::GetLanguages), &[]),
120 route("GET", "/repos/:owner/:name/contributors", Op::About(AboutOp::ListContributors), &[]),
121 route("GET", "/repos/:owner/:name/license", Op::About(AboutOp::GetLicense), &[]),
122 // Releases: `latest` and `tags/…` before an id.
123 route("GET", "/repos/:owner/:name/releases", Op::About(AboutOp::ListReleases), &[]),
124 route("POST", "/repos/:owner/:name/releases", Op::About(AboutOp::CreateRelease), &[]),
125 route("GET", "/repos/:owner/:name/releases/latest", Op::About(AboutOp::GetLatestRelease), &[]),
126 route("GET", "/repos/:owner/:name/releases/tags/:tag", Op::About(AboutOp::GetReleaseByTag), &[]),
127 route("GET", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::GetRelease), &[]),
128 route("PATCH", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::UpdateRelease), &[]),
129 route("DELETE", "/repos/:owner/:name/releases/:id", Op::About(AboutOp::DeleteRelease), &[]),
API: pinned projects over REST and MCP130 // Your pinned projects in a workspace, in your order.
131 route("GET", "/user/pinned_projects/:workspace", Op::ListPinnedProjects, &[]),
132 route("PUT", "/user/pinned_projects/:workspace", Op::ReorderPinnedProjects, &[]),
133 route("PUT", "/user/pinned_projects/:workspace/:project", Op::PinProject, &[]),
134 route("DELETE", "/user/pinned_projects/:workspace/:project", Op::UnpinProject, &[]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise135 route("PATCH", "/user/repository_invitations/:id", Op::AcceptRepoInvitation, &[]),
136 route("DELETE", "/user/repository_invitations/:id", Op::DeclineRepoInvitation, &[]),
137 route("PATCH", "/workspaces/:workspace", Op::UpdateWorkspace, &[]),
Merge main (membership, two-factor, GitHub repo roles) into tokens138 // Members and owners: GitHub's organization members, by username.
139 route("GET", "/workspaces/:workspace/members", Op::ListMembers, &[]),
140 route("PATCH", "/workspaces/:workspace/members/:username", Op::UpdateMember, &[]),
141 route("DELETE", "/workspaces/:workspace/members/:username", Op::RemoveMember, &[]),
142 route("POST", "/workspaces/:workspace/transfer_ownership", Op::TransferOwnership, &[]),
143 route("DELETE", "/user/memberships/:workspace", Op::LeaveWorkspace, &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97144 // A workspace's projects: what each is, where it runs, its links.
145 route("GET", "/workspaces/:workspace/projects", Op::ListProjects, &[]),
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.146 // What happened across its repositories: merged pull requests, releases, deployments, pushes.
147 route(
148 "GET",
149 "/workspaces/:workspace/activity",
150 Op::GetWorkspaceActivity,
151 &[("since", "since"), ("until", "until"), ("kinds", "kinds"), ("limit", "limit")],
152 ),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97153 route("GET", "/workspaces/:workspace/projects/:project", Op::GetProject, &[]),
154 route("PATCH", "/workspaces/:workspace/projects/:project", Op::UpdateProject, &[]),
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.155 // A project's observability: what its software did, and how it is collected.
156 route(
157 "GET",
158 "/workspaces/:workspace/projects/:project/observability/overview",
159 Op::Telemetry(TelemetryOp::GetOverview),
160 &[("from", "from"), ("to", "to"), ("environment", "environment"), ("service", "service"), ("deployment_id", "deployment_id"), ("step", "step")],
161 ),
162 route(
163 "GET",
164 "/workspaces/:workspace/projects/:project/observability/requests",
165 Op::Telemetry(TelemetryOp::ListRequests),
166 &[
167 ("from", "from"),
168 ("to", "to"),
169 ("q", "q"),
170 ("status", "status"),
171 ("errors_only", "errors_only"),
172 ("route", "route"),
173 ("min_duration_ms", "min_duration_ms"),
174 ("environment", "environment"),
175 ("service", "service"),
176 ("deployment_id", "deployment_id"),
177 ("cursor", "cursor"),
178 ("limit", "limit"),
179 ],
180 ),
181 route("GET", "/workspaces/:workspace/projects/:project/observability/requests/:id", Op::Telemetry(TelemetryOp::GetRequest), &[]),
182 route(
183 "GET",
184 "/workspaces/:workspace/projects/:project/observability/logs",
185 Op::Telemetry(TelemetryOp::ListLogs),
186 &[
187 ("from", "from"),
188 ("to", "to"),
189 ("q", "q"),
190 ("level", "level"),
191 ("trace_id", "trace_id"),
192 ("request_id", "request_id"),
193 ("environment", "environment"),
194 ("service", "service"),
195 ("deployment_id", "deployment_id"),
196 ("cursor", "cursor"),
197 ("limit", "limit"),
198 ],
199 ),
200 route(
201 "GET",
202 "/workspaces/:workspace/projects/:project/observability/traces",
203 Op::Telemetry(TelemetryOp::ListTraces),
204 &[
205 ("from", "from"),
206 ("to", "to"),
207 ("q", "q"),
208 ("errors_only", "errors_only"),
209 ("min_duration_ms", "min_duration_ms"),
210 ("environment", "environment"),
211 ("service", "service"),
212 ("cursor", "cursor"),
213 ("limit", "limit"),
214 ],
215 ),
216 route("GET", "/workspaces/:workspace/projects/:project/observability/traces/:trace_id", Op::Telemetry(TelemetryOp::GetTrace), &[]),
217 route(
218 "GET",
219 "/workspaces/:workspace/projects/:project/observability/errors",
220 Op::Telemetry(TelemetryOp::ListErrors),
221 &[("state", "state"), ("environment", "environment"), ("service", "service"), ("from", "from"), ("to", "to")],
222 ),
223 route("PATCH", "/workspaces/:workspace/projects/:project/observability/errors/:fingerprint", Op::Telemetry(TelemetryOp::UpdateError), &[]),
224 route("GET", "/workspaces/:workspace/projects/:project/observability/metrics", Op::Telemetry(TelemetryOp::ListMetrics), &[]),
225 route(
226 "GET",
227 "/workspaces/:workspace/projects/:project/observability/metrics/:metric",
228 Op::Telemetry(TelemetryOp::GetMetric),
229 &[("from", "from"), ("to", "to"), ("aggregate", "aggregate"), ("group_by", "group_by"), ("step", "step"), ("service", "service"), ("environment", "environment")],
230 ),
231 route("GET", "/workspaces/:workspace/projects/:project/observability/settings", Op::Telemetry(TelemetryOp::GetSettings), &[]),
232 route("PATCH", "/workspaces/:workspace/projects/:project/observability/settings", Op::Telemetry(TelemetryOp::UpdateSettings), &[]),
233 route("GET", "/workspaces/:workspace/projects/:project/observability/tokens", Op::Telemetry(TelemetryOp::ListTokens), &[]),
234 route("POST", "/workspaces/:workspace/projects/:project/observability/tokens", Op::Telemetry(TelemetryOp::CreateToken), &[]),
235 route("DELETE", "/workspaces/:workspace/projects/:project/observability/tokens/:id", Op::Telemetry(TelemetryOp::DeleteToken), &[]),
236 route("GET", "/workspaces/:workspace/projects/:project/observability/exports", Op::Telemetry(TelemetryOp::ListExports), &[]),
237 route("POST", "/workspaces/:workspace/projects/:project/observability/exports", Op::Telemetry(TelemetryOp::CreateExport), &[]),
238 route("PATCH", "/workspaces/:workspace/projects/:project/observability/exports/:id", Op::Telemetry(TelemetryOp::UpdateExport), &[]),
239 route("DELETE", "/workspaces/:workspace/projects/:project/observability/exports/:id", Op::Telemetry(TelemetryOp::DeleteExport), &[]),
240 route("GET", "/workspaces/:workspace/projects/:project/observability/usage", Op::Telemetry(TelemetryOp::GetUsage), &[]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise241 route("PUT", "/workspaces/:workspace/base_permission", Op::SetBasePermission, &[]),
242 route(
243 "GET",
244 "/workspaces/:workspace/outside_collaborators",
245 Op::ListOutsideCollaborators,
246 &[],
247 ),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar248 // Teams: a workspace's groups of members, with roles on repositories.
249 route("GET", "/workspaces/:workspace/teams", Op::ListTeams, &[("q", "query")]),
250 route("POST", "/workspaces/:workspace/teams", Op::CreateTeam, &[]),
251 route("GET", "/workspaces/:workspace/teams/:team", Op::GetTeam, &[]),
252 route("PATCH", "/workspaces/:workspace/teams/:team", Op::UpdateTeam, &[]),
253 route("DELETE", "/workspaces/:workspace/teams/:team", Op::DeleteTeam, &[]),
254 route(
255 "GET",
256 "/workspaces/:workspace/teams/:team/members",
257 Op::ListTeamMembers,
258 &[("include_child_teams", "include_child_teams")],
259 ),
260 route("PUT", "/workspaces/:workspace/teams/:team/members/:username", Op::SetTeamMember, &[]),
261 route("DELETE", "/workspaces/:workspace/teams/:team/members/:username", Op::RemoveTeamMember, &[]),
262 route("GET", "/workspaces/:workspace/teams/:team/teams", Op::ListChildTeams, &[]),
263 route("GET", "/workspaces/:workspace/teams/:team/repos", Op::ListTeamRepos, &[]),
264 route("PUT", "/workspaces/:workspace/teams/:team/repos/:repo", Op::SetTeamRepo, &[]),
265 route("DELETE", "/workspaces/:workspace/teams/:team/repos/:repo", Op::RemoveTeamRepo, &[]),
266 route(
267 "PUT",
268 "/workspaces/:workspace/teams/:team/review_assignment",
269 Op::SetTeamReviewAssignment,
270 &[],
271 ),
272 route("GET", "/workspaces/:workspace/members/:username/teams", Op::ListUserTeams, &[]),
Billing, integrations, security and identity no longer serve g1t's own agents. Billing stops metering agent runs, model tokens and AI Gateway requests, drops its model catalogue and price-keeper model prices, and stops selling AI credit, which only ever paid for those; credit bought earlier still pays toward what is owed. A usage report, statement or invoice that holds an old agent or model meter shows it as "Other usage" rather than failing, and the get_ai_credit and buy_ai_credit API operations and MCP billing actions go. Integrations no longer connects model providers or routes models, and no longer assigns imported issues or alerts to g1t; a model provider already connected can still be disconnected. Security drops "Fix with g1t". Identity no longer creates agent tokens or puts agents on teams, a team's lead is a person, and run credentials are issued only for checks, the merge queue, merge checks, deployments and bumps; tokens and credentials already issued keep working until they expire. The reserved names g1t and g1t-agent stay reserved. No tables are dropped.273 // A workspace's billing: usage, budget and invoices.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit274 route(
275 "GET",
276 "/workspaces/:workspace/usage",
277 Op::GetUsage,
278 &[("from", "from"), ("until", "until"), ("products", "products"), ("projects", "projects"), ("group_by", "group_by")],
279 ),
280 route("GET", "/workspaces/:workspace/budget", Op::GetBudget, &[]),
281 route("PUT", "/workspaces/:workspace/budget", Op::SetBudget, &[]),
282 route("GET", "/workspaces/:workspace/invoices", Op::ListInvoices, &[]),
283 route("GET", "/workspaces/:workspace/billing_details", Op::GetBillingDetails, &[]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar284 // Code owners: the CODEOWNERS file, checked.
285 route("GET", "/repos/:owner/:name/codeowners/errors", Op::GetCodeownersErrors, &[("ref", "ref")]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise286 // Security alerts: secrets and vulnerable dependencies.
287 route(
288 "GET",
289 "/repos/:owner/:name/security/alerts",
290 Op::ListSecurityAlerts,
291 &[("state", "state"), ("kind", "kind")],
292 ),
293 route("POST", "/repos/:owner/:name/security/alerts/:id/dismiss", Op::DismissSecurityAlert, &[]),
294 route("POST", "/repos/:owner/:name/security/alerts/:id/reopen", Op::ReopenSecurityAlert, &[]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar295 // The security suite: secret scanning, code scanning, vulnerability
296 // alerts and the supply chain, at the common addresses.
297 route("GET", "/repos/:owner/:name/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
298 route("GET", "/workspaces/:workspace/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
299 route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::GetSecretAlert), &[]),
300 route("PATCH", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::UpdateSecretAlert), &[]),
301 route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id/locations", Op::Security(SecurityOp::ListSecretLocations), &[]),
302 route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/bypass", Op::Security(SecurityOp::BypassPushProtection), &[]),
303 route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/validity", Op::Security(SecurityOp::CheckSecretValidity), &[]),
304 route("GET", "/workspaces/:workspace/secret-scanning/bypass-requests", Op::Security(SecurityOp::ListBypassRequests), &[("state", "state"), ("repo", "repo")]),
305 route("PATCH", "/workspaces/:workspace/secret-scanning/bypass-requests/:id", Op::Security(SecurityOp::ReviewBypassRequest), &[]),
306 route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
307 route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
308 route("GET", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
309 route("GET", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
310 route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
311 route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
312 route("PATCH", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
313 route("PATCH", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
314 route("DELETE", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
315 route("DELETE", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
316 route("GET", "/repos/:owner/:name/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
317 route("GET", "/workspaces/:workspace/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
318 route("GET", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::GetCodeAlert), &[]),
319 route("PATCH", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::UpdateCodeAlert), &[]),
320 route("GET", "/repos/:owner/:name/code-scanning/analyses", Op::Security(SecurityOp::ListAnalyses), &[]),
321 route("POST", "/repos/:owner/:name/code-scanning/sarifs", Op::Security(SecurityOp::UploadSarif), &[]),
322 route("GET", "/repos/:owner/:name/code-scanning/sarifs/:id", Op::Security(SecurityOp::GetSarifUpload), &[]),
323 route("GET", "/repos/:owner/:name/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
324 route("GET", "/workspaces/:workspace/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
325 route("GET", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::GetVulnerabilityAlert), &[]),
326 route("PATCH", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::UpdateVulnerabilityAlert), &[]),
327 route("GET", "/repos/:owner/:name/dependency-graph", Op::Security(SecurityOp::GetDependencyGraph), &[]),
328 route("GET", "/repos/:owner/:name/dependency-graph/sbom", Op::Security(SecurityOp::GetSbom), &[]),
329 route("GET", "/repos/:owner/:name/dependency-graph/compare/:basehead", Op::Security(SecurityOp::CompareDependencies), &[]),
330 route("GET", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::GetSettings), &[]),
331 route("PATCH", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::UpdateSettings), &[]),
332 route("GET", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::GetWorkspaceSettings), &[]),
333 route("PATCH", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::UpdateWorkspaceSettings), &[]),
334 route("GET", "/workspaces/:workspace/security/overview", Op::Security(SecurityOp::GetOverview), &[("days", "days")]),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9335 // Reporting: a repository's advisories, and the vulnerability reports sent to its maintainers.
336 route("GET", "/repos/:owner/:name/security/advisories", Op::Security(SecurityOp::ListAdvisories), &[("state", "state")]),
337 route("POST", "/repos/:owner/:name/security/advisories", Op::Security(SecurityOp::CreateAdvisory), &[]),
338 route("GET", "/repos/:owner/:name/security/advisories/:id", Op::Security(SecurityOp::GetAdvisory), &[]),
339 route("PATCH", "/repos/:owner/:name/security/advisories/:id", Op::Security(SecurityOp::UpdateAdvisory), &[]),
340 route("POST", "/repos/:owner/:name/security/advisories/:id/state", Op::Security(SecurityOp::SetAdvisoryState), &[]),
341 route("GET", "/repos/:owner/:name/security/reports", Op::Security(SecurityOp::ListReports), &[("state", "state")]),
342 route("POST", "/repos/:owner/:name/security/reports", Op::Security(SecurityOp::ReportVulnerability), &[]),
343 route("PATCH", "/repos/:owner/:name/security/reports/:id", Op::Security(SecurityOp::ReviewReport), &[]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise344 route("GET", "/repos", Op::ListRepos, &[("q", "query")]),
345 route(
346 "GET",
347 "/search",
348 Op::Search,
349 &[("q", "query"), ("type", "type"), ("page", "page"), ("per_page", "per_page")],
350 ),
351 route("POST", "/repos", Op::CreateRepo, &[]),
352 route("GET", "/repos/:owner/:name", Op::GetRepo, &[]),
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.353 // A file's text or a directory's entries: `path` and `ref` in the query.
354 route("GET", "/repos/:owner/:name/contents", Op::GetContents, &[("path", "path"), ("ref", "ref")]),
355 route("GET", "/repos/:owner/:name/commits", Op::ListCommits, &[("ref", "ref"), ("path", "path"), ("limit", "limit")]),
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.356 // A branch, tag or commit as one archive: a redirect to a link good for
357 // ten minutes, as GitHub's go to codeload.
358 route("GET", "/repos/:owner/:name/tarball/:ref", Op::Archives(ArchiveOp::DownloadTarball), &[]),
359 route("GET", "/repos/:owner/:name/zipball/:ref", Op::Archives(ArchiveOp::DownloadZipball), &[]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise360 route("PATCH", "/repos/:owner/:name", Op::UpdateRepo, &[]),
361 route("POST", "/repos/:owner/:name/transfer", Op::TransferRepo, &[]),
362 route("DELETE", "/repos/:owner/:name", Op::DeleteRepo, &[]),
363 route(
364 "GET",
365 "/workspaces/:workspace/repos/deleted",
366 Op::ListDeletedRepos,
367 &[],
368 ),
369 route("POST", "/repos/:owner/:name/restore", Op::RestoreRepo, &[]),
370 route("POST", "/repos/:owner/:name/purge", Op::PurgeRepo, &[]),
371 route("POST", "/repos/:owner/:name/rename", Op::RenameRepo, &[]),
372 route("POST", "/repos/:owner/:name/archive", Op::ArchiveRepo, &[]),
373 route("POST", "/repos/:owner/:name/unarchive", Op::UnarchiveRepo, &[]),
374 route(
375 "POST",
376 "/repos/:owner/:name/visibility",
377 Op::SetRepoVisibility,
378 &[],
379 ),
380 route(
381 "POST",
382 "/repos/:owner/:name/branches/:branch/rename",
383 Op::RenameBranch,
384 &[],
385 ),
The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days.386 route("POST", "/repos/:owner/:name/branches", Op::CreateBranch, &[]),
387 route("DELETE", "/repos/:owner/:name/branches/:branch", Op::DeleteBranch, &[("head", "head")]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise388 route(
389 "GET",
390 "/repos/:owner/:name/settings",
391 Op::GetRepoSettings,
392 &[],
393 ),
394 route(
395 "PATCH",
396 "/repos/:owner/:name/settings",
397 Op::UpdateRepoSettings,
398 &[],
399 ),
400 route("GET", "/repos/:owner/:name/check-names", Op::ListCheckNames, &[]),
Merge checks: statuses and check runs on every commit401 // Checks: GitHub's addresses for statuses, check runs and check suites.
402 route("POST", "/repos/:owner/:name/statuses/:sha", Op::Checks(ChecksOp::CreateCommitStatus), &[]),
403 route("GET", "/repos/:owner/:name/commits/:ref/statuses", Op::Checks(ChecksOp::ListCommitStatuses), &[]),
404 route("GET", "/repos/:owner/:name/commits/:ref/status", Op::Checks(ChecksOp::GetCombinedStatus), &[]),
405 route(
406 "GET",
407 "/repos/:owner/:name/commits/:ref/check-runs",
408 Op::Checks(ChecksOp::ListCheckRunsForRef),
409 &[("check_name", "check_name"), ("status", "status"), ("app", "app"), ("filter", "filter")],
410 ),
411 route(
412 "GET",
413 "/repos/:owner/:name/commits/:ref/check-suites",
414 Op::Checks(ChecksOp::ListCheckSuitesForRef),
415 &[("app", "app"), ("check_name", "check_name")],
416 ),
417 route("POST", "/repos/:owner/:name/check-runs", Op::Checks(ChecksOp::CreateCheckRun), &[]),
418 route("GET", "/repos/:owner/:name/check-runs/:id", Op::Checks(ChecksOp::GetCheckRun), &[]),
419 route("PATCH", "/repos/:owner/:name/check-runs/:id", Op::Checks(ChecksOp::UpdateCheckRun), &[]),
420 route("GET", "/repos/:owner/:name/check-runs/:id/annotations", Op::Checks(ChecksOp::ListCheckRunAnnotations), &[]),
421 route("POST", "/repos/:owner/:name/check-runs/:id/rerequest", Op::Checks(ChecksOp::RerequestCheckRun), &[]),
422 route("GET", "/repos/:owner/:name/check-suites/:id", Op::Checks(ChecksOp::GetCheckSuite), &[]),
423 route("POST", "/repos/:owner/:name/check-suites/:id/rerequest", Op::Checks(ChecksOp::RerequestCheckSuite), &[]),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge424 // Rulesets: a repository's, a workspace's, the rules of one branch,
425 // and how they judged pushes and merges.
426 route("GET", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::ListRepoRulesets), &[("include_parents", "include_parents")]),
427 route("POST", "/repos/:owner/:name/rulesets", Op::Rules(RulesOp::CreateRepoRuleset), &[]),
428 route("GET", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::GetRepoRuleset), &[]),
429 route("PUT", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::UpdateRepoRuleset), &[]),
430 route("DELETE", "/repos/:owner/:name/rulesets/:id", Op::Rules(RulesOp::DeleteRepoRuleset), &[]),
431 route("GET", "/repos/:owner/:name/rules/branches/:branch", Op::Rules(RulesOp::GetBranchRules), &[("target", "target")]),
432 route(
433 "GET",
434 "/repos/:owner/:name/rules/evaluations",
435 Op::Rules(RulesOp::ListRuleEvaluations),
436 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
437 ),
438 route("GET", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::ListWorkspaceRulesets), &[]),
439 route("POST", "/workspaces/:workspace/rulesets", Op::Rules(RulesOp::CreateWorkspaceRuleset), &[]),
440 route("GET", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::GetWorkspaceRuleset), &[]),
441 route("PUT", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::UpdateWorkspaceRuleset), &[]),
442 route("DELETE", "/workspaces/:workspace/rulesets/:id", Op::Rules(RulesOp::DeleteWorkspaceRuleset), &[]),
443 route(
444 "GET",
445 "/workspaces/:workspace/rules/evaluations",
446 Op::Rules(RulesOp::ListWorkspaceRuleEvaluations),
447 &[("ruleset_id", "ruleset_id"), ("verdict", "verdict"), ("problems_only", "problems_only"), ("before", "before"), ("limit", "limit")],
448 ),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97449 // Deployments wherever they run, their statuses, and environments.
450 route(
451 "GET",
452 "/repos/:owner/:name/deployments",
453 Op::Deployments(DeploymentsOp::ListDeployments),
454 &[("environment", "environment"), ("ref", "ref"), ("sha", "sha"), ("task", "task"), ("state", "state"), ("source", "source"), ("creator", "creator"), ("page", "page"), ("per_page", "per_page")],
455 ),
456 route("POST", "/repos/:owner/:name/deployments", Op::Deployments(DeploymentsOp::CreateDeployment), &[]),
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9457 // A g1t Actions job opening a g1t.page build it runs itself.
458 route("POST", "/repos/:owner/:name/deployments/builds", Op::Deployments(DeploymentsOp::StartDeploymentBuild), &[]),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97459 route("GET", "/repos/:owner/:name/deployments/:id", Op::Deployments(DeploymentsOp::GetDeployment), &[]),
460 route("GET", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::ListDeploymentStatuses), &[]),
461 route("POST", "/repos/:owner/:name/deployments/:id/statuses", Op::Deployments(DeploymentsOp::CreateDeploymentStatus), &[]),
462 route("GET", "/repos/:owner/:name/environments", Op::Deployments(DeploymentsOp::ListEnvironments), &[]),
463 route("GET", "/repos/:owner/:name/environments/:environment", Op::Deployments(DeploymentsOp::GetEnvironment), &[]),
Merge branch 'worktree-agent-a3abfcce648e87dca'464 // Environments' protection rules, and the runs they hold.
465 route("PUT", "/repos/:owner/:name/environments/:environment", Op::Protection(ProtectionOp::UpdateEnvironment), &[]),
466 route("DELETE", "/repos/:owner/:name/environments/:environment", Op::Protection(ProtectionOp::DeleteEnvironment), &[]),
467 route(
468 "GET",
469 "/repos/:owner/:name/actions/runs/:id/pending_deployments",
470 Op::Protection(ProtectionOp::GetPendingDeployments),
471 &[],
472 ),
473 route(
474 "POST",
475 "/repos/:owner/:name/actions/runs/:id/pending_deployments",
476 Op::Protection(ProtectionOp::ReviewPendingDeployments),
477 &[],
478 ),
479 route("POST", "/repos/:owner/:name/actions/runs/:id/approve", Op::Protection(ProtectionOp::ApproveWorkflowRun), &[]),
480 route("GET", "/repos/:owner/:name/actions/permissions/workflow", Op::Protection(ProtectionOp::GetWorkflowPermissions), &[]),
481 route("PUT", "/repos/:owner/:name/actions/permissions/workflow", Op::Protection(ProtectionOp::SetWorkflowPermissions), &[]),
482 route(
483 "GET",
484 "/repos/:owner/:name/actions/permissions/fork-pr-contributor-approval",
485 Op::Protection(ProtectionOp::GetForkPrApproval),
486 &[],
487 ),
488 route(
489 "PUT",
490 "/repos/:owner/:name/actions/permissions/fork-pr-contributor-approval",
491 Op::Protection(ProtectionOp::SetForkPrApproval),
492 &[],
493 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts494 route("GET", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::GetActionsAccess), &[]),
495 route("PUT", "/repos/:owner/:name/actions/permissions/access", Op::Protection(ProtectionOp::SetActionsAccess), &[]),
Merge branch 'worktree-agent-a3abfcce648e87dca'496 route("POST", "/repos/:owner/:name/dispatches", Op::Protection(ProtectionOp::CreateRepositoryDispatch), &[]),
497 route(
498 "GET",
499 "/workspaces/:workspace/actions/permissions/workflow",
500 Op::Protection(ProtectionOp::GetWorkspaceWorkflowPermissions),
501 &[],
502 ),
503 route(
504 "PUT",
505 "/workspaces/:workspace/actions/permissions/workflow",
506 Op::Protection(ProtectionOp::SetWorkspaceWorkflowPermissions),
507 &[],
508 ),
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.509 route(
510 "GET",
511 "/workspaces/:workspace/actions/managed-workflows",
512 Op::Protection(ProtectionOp::ListWorkspaceManagedWorkflows),
513 &[],
514 ),
515 route(
516 "POST",
517 "/workspaces/:workspace/actions/managed-workflows",
518 Op::Protection(ProtectionOp::CreateWorkspaceManagedWorkflow),
519 &[],
520 ),
521 route(
522 "PATCH",
523 "/workspaces/:workspace/actions/managed-workflows/:workflow",
524 Op::Protection(ProtectionOp::UpdateWorkspaceManagedWorkflow),
525 &[],
526 ),
527 route(
528 "DELETE",
529 "/workspaces/:workspace/actions/managed-workflows/:workflow",
530 Op::Protection(ProtectionOp::DeleteWorkspaceManagedWorkflow),
531 &[],
532 ),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise533 route("GET", "/repos/:owner/:name/queue", Op::GetMergeQueue, &[]),
534 route("POST", "/repos/:owner/:name/memory", Op::Remember, &[]),
535 route(
536 "GET",
537 "/repos/:owner/:name/memory",
538 Op::Recall,
539 &[("q", "query"), ("limit", "limit")],
540 ),
541 route(
542 "GET",
543 "/repos/:owner/:name/events",
544 Op::ListEvents,
Merge branch 'worktree-agent-ad531323a21f4c975'545 &[
546 ("before", "before"),
547 ("after", "after"),
548 ("types", "types"),
549 ("numbers", "numbers"),
550 ("since", "since"),
551 ("limit", "limit"),
552 ("wait", "wait"),
553 ],
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise554 ),
555 route("GET", "/repos/:owner/:name/labels", Op::ListLabels, &[]),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar556 route("POST", "/repos/:owner/:name/labels", Op::CreateLabel, &[]),
557 route("POST", "/repos/:owner/:name/labels/defaults", Op::AddDefaultLabels, &[]),
558 route("PATCH", "/repos/:owner/:name/labels/:label", Op::UpdateLabel, &[]),
559 route("DELETE", "/repos/:owner/:name/labels/:label", Op::DeleteLabel, &[]),
560 route("GET", "/repos/:owner/:name/issues/:number/labels", Op::ListIssueLabels, &[]),
561 route("POST", "/repos/:owner/:name/issues/:number/labels", Op::AddIssueLabels, &[]),
562 route("PUT", "/repos/:owner/:name/issues/:number/labels", Op::SetIssueLabels, &[]),
563 route("DELETE", "/repos/:owner/:name/issues/:number/labels", Op::RemoveIssueLabels, &[]),
564 route("DELETE", "/repos/:owner/:name/issues/:number/labels/:label", Op::RemoveIssueLabels, &[]),
565 route("GET", "/repos/:owner/:name/milestones", Op::ListMilestones, &[("state", "state")]),
566 route("POST", "/repos/:owner/:name/milestones", Op::CreateMilestone, &[]),
567 route("GET", "/repos/:owner/:name/milestones/:milestone", Op::GetMilestone, &[]),
568 route("PATCH", "/repos/:owner/:name/milestones/:milestone", Op::UpdateMilestone, &[]),
569 route("DELETE", "/repos/:owner/:name/milestones/:milestone", Op::DeleteMilestone, &[]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise570 route(
571 "GET",
572 "/repos/:owner/:name/issues",
573 Op::ListIssues,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar574 &[("state", "state"), ("label", "label"), ("milestone", "milestone")],
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise575 ),
576 route("POST", "/repos/:owner/:name/issues", Op::CreateIssue, &[]),
577 route(
578 "GET",
579 "/repos/:owner/:name/issues/:number",
580 Op::GetIssue,
581 &[],
582 ),
583 route(
584 "PATCH",
585 "/repos/:owner/:name/issues/:number",
586 Op::UpdateIssue,
587 &[],
588 ),
589 route(
590 "POST",
591 "/repos/:owner/:name/issues/:number/close",
592 Op::CloseIssue,
593 &[],
594 ),
595 route(
596 "POST",
597 "/repos/:owner/:name/issues/:number/reopen",
598 Op::ReopenIssue,
599 &[],
600 ),
601 route(
602 "POST",
603 "/repos/:owner/:name/issues/import",
604 Op::ImportIssue,
605 &[],
606 ),
Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae'607 // Coordinating work: claims on issues, an issue's whole context, and
608 // sessions of people's own harnesses.
609 route("GET", "/repos/:owner/:name/issues/:number/work", Op::Coordination(CoordinationOp::GetWork), &[]),
610 route("POST", "/repos/:owner/:name/issues/:number/claim", Op::Coordination(CoordinationOp::ClaimIssue), &[]),
611 route("POST", "/repos/:owner/:name/issues/:number/release", Op::Coordination(CoordinationOp::ReleaseClaim), &[]),
612 route("POST", "/repos/:owner/:name/issues/:number/handoff", Op::Coordination(CoordinationOp::HandOffIssue), &[]),
613 route(
614 "GET",
615 "/repos/:owner/:name/claims",
616 Op::Coordination(CoordinationOp::ListClaims),
617 &[("state", "state"), ("issue", "issue")],
618 ),
619 route("POST", "/repos/:owner/:name/sessions", Op::Coordination(CoordinationOp::StartSession), &[]),
620 route(
621 "GET",
622 "/repos/:owner/:name/sessions",
623 Op::Coordination(CoordinationOp::ListSessions),
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.624 &[("state", "state"), ("issue", "issue"), ("pull", "pull"), ("path", "path"), ("subagents", "subagents")],
Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae'625 ),
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.626 route("POST", "/repos/:owner/:name/agent-sessions", Op::Coordination(CoordinationOp::StartAgentSession), &[]),
Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae'627 route("GET", "/repos/:owner/:name/sessions/:id", Op::Coordination(CoordinationOp::GetSession), &[("after", "after")]),
628 route("PATCH", "/repos/:owner/:name/sessions/:id", Op::Coordination(CoordinationOp::UpdateSession), &[]),
629 route("POST", "/repos/:owner/:name/sessions/:id/end", Op::Coordination(CoordinationOp::EndSession), &[]),
630 route(
631 "POST",
632 "/repos/:owner/:name/sessions/:id/entries",
633 Op::Coordination(CoordinationOp::RecordSessionEntries),
634 &[],
635 ),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise636 route(
637 "GET",
638 "/repos/:owner/:name/context",
639 Op::GetContext,
640 &[("reference", "reference")],
641 ),
642 route(
643 "GET",
644 "/workspaces/:workspace/integrations",
645 Op::ListIntegrations,
646 &[],
647 ),
648 route(
649 "POST",
650 "/workspaces/:workspace/integrations",
651 Op::ConnectIntegration,
652 &[],
653 ),
654 route(
655 "GET",
656 "/repos/:owner/:name/hooks",
657 Op::ListWebhooks,
658 &[],
659 ),
660 route(
661 "POST",
662 "/repos/:owner/:name/hooks",
663 Op::CreateWebhook,
664 &[],
665 ),
666 route(
667 "PATCH",
668 "/repos/:owner/:name/hooks/:id",
669 Op::UpdateWebhook,
670 &[],
671 ),
672 route(
673 "DELETE",
674 "/repos/:owner/:name/hooks/:id",
675 Op::DeleteWebhook,
676 &[],
677 ),
678 route(
679 "POST",
680 "/repos/:owner/:name/hooks/:id/pings",
681 Op::PingWebhook,
682 &[],
683 ),
684 route(
685 "GET",
686 "/repos/:owner/:name/hooks/:id/deliveries",
687 Op::ListWebhookDeliveries,
688 &[],
689 ),
690 route(
691 "POST",
692 "/repos/:owner/:name/hooks/:id/deliveries/:delivery/redeliver",
693 Op::RedeliverWebhook,
694 &[],
695 ),
696 route(
697 "GET",
698 "/workspaces/:workspace/hooks",
699 Op::ListWebhooks,
700 &[],
701 ),
702 route(
703 "POST",
704 "/workspaces/:workspace/hooks",
705 Op::CreateWebhook,
706 &[],
707 ),
708 route(
709 "PATCH",
710 "/workspaces/:workspace/hooks/:id",
711 Op::UpdateWebhook,
712 &[],
713 ),
714 route(
715 "DELETE",
716 "/workspaces/:workspace/hooks/:id",
717 Op::DeleteWebhook,
718 &[],
719 ),
720 route(
721 "POST",
722 "/workspaces/:workspace/hooks/:id/pings",
723 Op::PingWebhook,
724 &[],
725 ),
726 route(
727 "GET",
728 "/workspaces/:workspace/hooks/:id/deliveries",
729 Op::ListWebhookDeliveries,
730 &[],
731 ),
732 route(
733 "POST",
734 "/workspaces/:workspace/hooks/:id/deliveries/:delivery/redeliver",
735 Op::RedeliverWebhook,
736 &[],
737 ),
738 route(
AI Gateway: OpenAI's format, open models, and your own providers739 "PATCH",
740 "/workspaces/:workspace/integrations/:id",
741 Op::UpdateIntegration,
742 &[],
743 ),
744 route(
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise745 "DELETE",
746 "/workspaces/:workspace/integrations/:id",
747 Op::DisconnectIntegration,
748 &[],
749 ),
750 route(
751 "POST",
752 "/workspaces/:workspace/integrations/:id/test",
753 Op::TestIntegration,
754 &[],
755 ),
756 route(
757 "GET",
758 "/repos/:owner/:name/actions/workflows",
759 Op::ListWorkflows,
760 &[],
761 ),
762 route(
763 "GET",
Merge branch 'worktree-agent-a4db4b523bb973765'764 "/repos/:owner/:name/actions/workflows/:workflow",
765 Op::GetWorkflow,
766 &[("ref", "ref")],
767 ),
768 route(
769 "GET",
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise770 "/repos/:owner/:name/actions/workflows/:workflow/runs",
771 Op::ListWorkflowRuns,
772 &[("branch", "branch"), ("event", "event"), ("per_page", "limit")],
773 ),
774 route(
775 "POST",
776 "/repos/:owner/:name/actions/workflows/:workflow/dispatches",
777 Op::DispatchWorkflow,
778 &[],
779 ),
780 route(
781 "PATCH",
782 "/repos/:owner/:name/actions/workflows/:workflow",
783 Op::UpdateWorkflow,
784 &[],
785 ),
786 route(
787 "PUT",
788 "/repos/:owner/:name/actions/workflows/:workflow/enable",
789 Op::UpdateWorkflow,
790 &[],
791 ),
792 route(
793 "PUT",
794 "/repos/:owner/:name/actions/workflows/:workflow/disable",
795 Op::UpdateWorkflow,
796 &[],
797 ),
798 route(
799 "GET",
800 "/repos/:owner/:name/actions/runs",
801 Op::ListWorkflowRuns,
802 &[("workflow", "workflow"), ("branch", "branch"), ("event", "event"), ("pull", "pull"), ("head_sha", "sha"), ("per_page", "limit")],
803 ),
804 route(
805 "GET",
806 "/repos/:owner/:name/actions/runs/:id",
807 Op::GetWorkflowRun,
808 &[],
809 ),
810 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)811 "GET",
812 "/repos/:owner/:name/actions/runs/:id/attempts/:attempt",
813 Op::GetWorkflowRun,
814 &[],
815 ),
816 route(
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise817 "POST",
818 "/repos/:owner/:name/actions/runs/:id/cancel",
819 Op::CancelWorkflowRun,
820 &[],
821 ),
822 route(
823 "POST",
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)824 "/repos/:owner/:name/actions/runs/:id/force-cancel",
825 Op::CancelWorkflowRun,
826 &[],
827 ),
828 route(
829 "POST",
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise830 "/repos/:owner/:name/actions/runs/:id/rerun",
831 Op::RerunWorkflowRun,
832 &[],
833 ),
834 route(
835 "POST",
836 "/repos/:owner/:name/actions/runs/:id/rerun-failed-jobs",
837 Op::RerunWorkflowRun,
838 &[],
839 ),
840 route(
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)841 "POST",
842 "/repos/:owner/:name/actions/jobs/:job/rerun",
843 Op::RerunWorkflowRun,
844 &[],
845 ),
846 route(
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise847 "GET",
848 "/repos/:owner/:name/actions/jobs/:job/logs",
849 Op::GetJobLogs,
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.850 &[("after", "after"), ("tail", "tail")],
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise851 ),
Merge packages: roles, Actions access, source label, soft delete, API852 // Packages, at GitHub's addresses with the workspace in place of the
853 // organization. A name with a slash is one URL-encoded segment.
854 route("GET", "/workspaces/:workspace/packages", Op::Packages(PackagesOp::ListPackages), &[("package_type", "package_type"), ("q", "q"), ("state", "state")]),
855 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::GetPackage), &[]),
856 route("PATCH", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::UpdatePackage), &[]),
857 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name", Op::Packages(PackagesOp::DeletePackage), &[]),
858 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/restore", Op::Packages(PackagesOp::RestorePackage), &[]),
859 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions", Op::Packages(PackagesOp::ListVersions), &[("state", "state")]),
860 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::GetVersion), &[]),
861 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id", Op::Packages(PackagesOp::DeleteVersion), &[]),
862 route("POST", "/workspaces/:workspace/packages/:package_type/:package_name/versions/:version_id/restore", Op::Packages(PackagesOp::RestoreVersion), &[]),
863 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::LinkPackage), &[]),
864 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/repository", Op::Packages(PackagesOp::UnlinkPackage), &[]),
865 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::ListAccess), &[]),
866 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::SetAccess), &[]),
867 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/access", Op::Packages(PackagesOp::RemoveAccess), &[("username", "username"), ("team", "team")]),
868 route("GET", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::ListActionsAccess), &[]),
869 route("PUT", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access", Op::Packages(PackagesOp::SetActionsAccess), &[]),
870 route("DELETE", "/workspaces/:workspace/packages/:package_type/:package_name/actions-access/:repository", Op::Packages(PackagesOp::RemoveActionsAccess), &[]),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2871 // Artifacts, at GitHub's addresses. `…/zip` answers with a redirect to
872 // a signed link (lib.rs).
873 route("GET", "/repos/:owner/:name/actions/artifacts", Op::Artifacts(ArtifactsOp::ListArtifacts), &[("name", "name"), ("page", "page"), ("per_page", "per_page")]),
874 route("GET", "/repos/:owner/:name/actions/runs/:id/artifacts", Op::Artifacts(ArtifactsOp::ListRunArtifacts), &[("name", "name")]),
875 route("GET", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::GetArtifact), &[]),
876 route("GET", "/repos/:owner/:name/actions/artifacts/:id/zip", Op::Artifacts(ArtifactsOp::DownloadArtifact), &[]),
877 route("DELETE", "/repos/:owner/:name/actions/artifacts/:id", Op::Artifacts(ArtifactsOp::DeleteArtifact), &[]),
878 route("GET", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::GetArtifactRetention), &[]),
879 route("PUT", "/repos/:owner/:name/actions/permissions/artifact-and-log-retention", Op::Artifacts(ArtifactsOp::SetArtifactRetention), &[]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise880 route(
881 "GET",
Merge branch 'worktree-agent-a3b9b1f730f55b5bf'882 "/repos/:owner/:name/actions/caches",
883 Op::Caches(CachesOp::ListCaches),
884 &[("key", "key"), ("ref", "ref"), ("sort", "sort"), ("direction", "direction"), ("page", "page"), ("per_page", "per_page")],
885 ),
886 route("GET", "/repos/:owner/:name/actions/cache/usage", Op::Caches(CachesOp::GetCacheUsage), &[]),
887 route("DELETE", "/repos/:owner/:name/actions/caches", Op::Caches(CachesOp::DeleteCachesByKey), &[("key", "key"), ("ref", "ref")]),
888 route("DELETE", "/repos/:owner/:name/actions/caches/:id", Op::Caches(CachesOp::DeleteCache), &[]),
889 route(
890 "GET",
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise891 "/repos/:owner/:name/actions/secrets",
892 Op::ListActionsSecrets,
893 &[],
894 ),
895 route(
896 "PUT",
897 "/repos/:owner/:name/actions/secrets/:setting",
898 Op::SetActionsSecret,
899 &[],
900 ),
901 route(
902 "DELETE",
903 "/repos/:owner/:name/actions/secrets/:setting",
904 Op::DeleteActionsSecret,
905 &[],
906 ),
907 route(
908 "GET",
909 "/repos/:owner/:name/actions/variables",
910 Op::ListActionsVariables,
911 &[],
912 ),
913 route(
914 "POST",
915 "/repos/:owner/:name/actions/variables",
916 Op::SetActionsVariable,
917 &[],
918 ),
919 route(
920 "PATCH",
921 "/repos/:owner/:name/actions/variables/:setting",
922 Op::SetActionsVariable,
923 &[],
924 ),
925 route(
926 "DELETE",
927 "/repos/:owner/:name/actions/variables/:setting",
928 Op::DeleteActionsVariable,
929 &[],
930 ),
931 route(
932 "GET",
933 "/workspaces/:workspace/actions/secrets",
934 Op::ListActionsSecrets,
935 &[],
936 ),
937 route(
938 "PUT",
939 "/workspaces/:workspace/actions/secrets/:setting",
940 Op::SetActionsSecret,
941 &[],
942 ),
943 route(
944 "DELETE",
945 "/workspaces/:workspace/actions/secrets/:setting",
946 Op::DeleteActionsSecret,
947 &[],
948 ),
949 route(
950 "GET",
951 "/workspaces/:workspace/actions/variables",
952 Op::ListActionsVariables,
953 &[],
954 ),
955 route(
956 "POST",
957 "/workspaces/:workspace/actions/variables",
958 Op::SetActionsVariable,
959 &[],
960 ),
961 route(
962 "PATCH",
963 "/workspaces/:workspace/actions/variables/:setting",
964 Op::SetActionsVariable,
965 &[],
966 ),
967 route(
968 "DELETE",
969 "/workspaces/:workspace/actions/variables/:setting",
970 Op::DeleteActionsVariable,
971 &[],
972 ),
973 // Self-hosted runners: a repository's own, or a workspace's.
974 route("GET", "/repos/:owner/:name/actions/runners", Op::ListRunners, &[]),
975 route("POST", "/repos/:owner/:name/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
976 route("DELETE", "/repos/:owner/:name/actions/runners/:id", Op::RemoveRunner, &[]),
977 route("GET", "/repos/:owner/:name/actions/runner-settings", Op::GetRunnerSettings, &[]),
978 route("PATCH", "/repos/:owner/:name/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
979 route("GET", "/workspaces/:workspace/actions/runners", Op::ListRunners, &[]),
980 route("POST", "/workspaces/:workspace/actions/runners/registration-token", Op::CreateRunnerRegistrationToken, &[]),
981 route("DELETE", "/workspaces/:workspace/actions/runners/:id", Op::RemoveRunner, &[]),
982 route("GET", "/workspaces/:workspace/actions/runner-settings", Op::GetRunnerSettings, &[]),
983 route("PATCH", "/workspaces/:workspace/actions/runner-settings", Op::UpdateRunnerSettings, &[]),
984 route("GET", "/workspaces/:workspace/actions/runner-groups", Op::ListRunnerGroups, &[]),
985 route("POST", "/workspaces/:workspace/actions/runner-groups", Op::CreateRunnerGroup, &[]),
986 route("PATCH", "/workspaces/:workspace/actions/runner-groups/:id", Op::UpdateRunnerGroup, &[]),
987 route("DELETE", "/workspaces/:workspace/actions/runner-groups/:id", Op::DeleteRunnerGroup, &[]),
988 route(
989 "POST",
990 "/repos/:owner/:name/issues/:number/comments",
991 Op::AddComment,
992 &[],
993 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts994 route("PATCH", "/repos/:owner/:name/issues/comments/:comment_id", Op::EditComment, &[]),
995 route("DELETE", "/repos/:owner/:name/issues/comments/:comment_id", Op::DeleteComment, &[]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise996 route(
997 "GET",
998 "/repos/:owner/:name/pulls",
999 Op::ListPullRequests,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1000 &[("state", "state"), ("label", "label"), ("milestone", "milestone"), ("base", "base")],
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1001 ),
1002 route(
1003 "POST",
1004 "/repos/:owner/:name/pulls",
1005 Op::CreatePullRequest,
1006 &[],
1007 ),
1008 route(
1009 "GET",
1010 "/repos/:owner/:name/pulls/:number",
1011 Op::GetPullRequest,
1012 &[],
1013 ),
1014 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1015 "PATCH",
1016 "/repos/:owner/:name/pulls/:number",
1017 Op::UpdatePullRequest,
1018 &[],
1019 ),
1020 route(
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1021 "GET",
1022 "/repos/:owner/:name/pulls/:number/changes",
1023 Op::GetPullRequestChanges,
1024 &[],
1025 ),
1026 route(
1027 "POST",
1028 "/repos/:owner/:name/pulls/:number/reviews",
1029 Op::ReviewPullRequest,
1030 &[],
1031 ),
1032 route(
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1033 "POST",
1034 "/repos/:owner/:name/pulls/:number/requested_reviewers",
1035 Op::RequestReviewers,
1036 &[],
1037 ),
1038 route(
1039 "DELETE",
1040 "/repos/:owner/:name/pulls/:number/requested_reviewers",
1041 Op::RemoveRequestedReviewers,
1042 &[],
1043 ),
1044 route(
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1045 "GET",
1046 "/repos/:owner/:name/pulls/:number/session",
1047 Op::ReadSession,
1048 &[("after", "after")],
1049 ),
1050 route(
1051 "POST",
1052 "/repos/:owner/:name/pulls/:number/session",
1053 Op::RecordSession,
1054 &[],
1055 ),
Merge branch 'worktree-agent-ab8033ab973f8d99a'1056 // Decision records, on a pull request or an issue (with its pull
1057 // requests'), and why a line is the way it is.
1058 route("GET", "/repos/:owner/:name/pulls/:number/decisions", Op::ListDecisions, &[]),
1059 route("POST", "/repos/:owner/:name/pulls/:number/decisions", Op::RecordDecision, &[]),
1060 route("GET", "/repos/:owner/:name/issues/:number/decisions", Op::ListDecisions, &[]),
1061 route("GET", "/repos/:owner/:name/why", Op::Why, &[("path", "path"), ("line", "line"), ("ref", "ref")]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1062 route(
1063 "POST",
1064 "/repos/:owner/:name/pulls/:number/ready",
1065 Op::MarkPullRequestReady,
1066 &[],
1067 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1068 route("POST", "/repos/:owner/:name/pulls/:number/draft", Op::ConvertPullRequestToDraft, &[]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1069 route(
1070 "POST",
1071 "/repos/:owner/:name/pulls/:number/close",
1072 Op::ClosePullRequest,
1073 &[],
1074 ),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1075 route("POST", "/repos/:owner/:name/pulls/:number/reopen", Op::ReopenPullRequest, &[]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1076 route(
1077 "POST",
1078 "/repos/:owner/:name/pulls/:number/merge",
1079 Op::MergePullRequest,
1080 &[],
1081 ),
Merge main into Artifacts Phase 21082 // Artifacts mode's docs, slides, designs and dashboards. Search comes
1083 // before an artifact by id, which it would otherwise match.
1084 route("GET", "/workspaces/:workspace/artifacts", Op::Folios(FoliosOp::List), &[("tab", "tab"), ("kind", "kind"), ("space", "space"), ("project", "project"), ("q", "q"), ("state", "state"), ("cursor", "cursor"), ("limit", "limit")]),
1085 route("POST", "/workspaces/:workspace/artifacts", Op::Folios(FoliosOp::Create), &[]),
1086 route("GET", "/workspaces/:workspace/artifacts/search", Op::Folios(FoliosOp::Search), &[("q", "q"), ("kind", "kind"), ("space", "space"), ("project", "project"), ("limit", "limit")]),
1087 route("GET", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Get), &[]),
1088 route("PATCH", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Update), &[]),
1089 route("DELETE", "/workspaces/:workspace/artifacts/:artifact_id", Op::Folios(FoliosOp::Trash), &[]),
1090 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/restore", Op::Folios(FoliosOp::Restore), &[]),
1091 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/purge", Op::Folios(FoliosOp::Purge), &[]),
1092 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::GetContent), &[]),
1093 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/content", Op::Folios(FoliosOp::Edit), &[]),
1094 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::GetAccess), &[]),
1095 route("PUT", "/workspaces/:workspace/artifacts/:artifact_id/access", Op::Folios(FoliosOp::SetAccess), &[]),
1096 route("GET", "/workspaces/:workspace/artifacts/:artifact_id/versions", Op::Folios(FoliosOp::ListVersions), &[]),
1097 route("POST", "/workspaces/:workspace/artifacts/:artifact_id/versions/:version_id/restore", Op::Folios(FoliosOp::RestoreVersion), &[]),
1098 route("GET", "/workspaces/:workspace/artifact-templates", Op::Folios(FoliosOp::ListTemplates), &[("kind", "kind")]),
1099 route("GET", "/workspaces/:workspace/artifact-spaces", Op::Folios(FoliosOp::ListSpaces), &[]),
1100 route("POST", "/workspaces/:workspace/datasets/query", Op::Folios(FoliosOp::QueryDataset), &[]),
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1101];
1102
1103impl Route {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1104 /// Whether the route answers success with `204` and no body, as
1105 /// GitHub's address for the same does. MCP still answers `true`.
1106 pub fn no_content(&self) -> bool {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r91107 matches!(
1108 self.op,
Merge branch 'worktree-agent-a3b9b1f730f55b5bf'1109 Op::DeleteComment | Op::Caches(CachesOp::DeleteCache)
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r91110 )
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1111 }
1112
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1113 /// The names of the route's path parameters, in order.
1114 pub fn params(&self) -> impl Iterator<Item = &'static str> {
1115 self.path
1116 .split('/')
1117 .filter_map(|segment| segment.strip_prefix(':'))
1118 }
1119
1120 /// The values of the path parameters, if `path` is this route's.
1121 fn matches<'a>(&self, path: &'a str) -> Option<Vec<(&'static str, &'a str)>> {
1122 let mut values = Vec::new();
1123 let mut actual = path.trim_end_matches('/').split('/');
1124 for expected in self.path.split('/') {
1125 let segment = actual.next()?;
1126 match expected.strip_prefix(':') {
1127 Some(name) if !segment.is_empty() => values.push((name, segment)),
1128 Some(_) => return None,
1129 None if expected == segment => {}
1130 None => return None,
1131 }
1132 }
1133 actual.next().is_none().then_some(values)
1134 }
1135}
1136
1137/// A path segment with its `%XX` escapes decoded; as given when that is not
1138/// UTF-8.
1139fn percent_decoded(segment: &str) -> String {
1140 let bytes = segment.as_bytes();
1141 let mut out = Vec::with_capacity(bytes.len());
1142 let mut i = 0;
1143 while i < bytes.len() {
1144 let escaped = (bytes[i] == b'%')
1145 .then(|| segment.get(i + 1..i + 3))
1146 .flatten()
1147 .filter(|hex| hex.bytes().all(|byte| byte.is_ascii_hexdigit()))
1148 .and_then(|hex| u8::from_str_radix(hex, 16).ok());
1149 match escaped {
1150 Some(byte) => {
1151 out.push(byte);
1152 i += 3;
1153 }
1154 None => {
1155 out.push(bytes[i]);
1156 i += 1;
1157 }
1158 }
1159 }
1160 String::from_utf8(out).unwrap_or_else(|_| segment.to_owned())
1161}
1162
1163/// The route for a request, and the operation input it describes.
1164///
1165/// The input is the JSON body, overlaid with the query parameters the route
1166/// reads and then with what the path names: `owner` and `name` become
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1167/// `repo`, as does a team's `repo` with its `workspace`, and `number`
1168/// becomes an integer.
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1169pub fn resolve(
1170 method: &str,
1171 path: &str,
1172 query: &[(String, String)],
1173 body: Value,
1174) -> Option<(&'static Route, Value)> {
1175 let (route, params) = ROUTES
1176 .iter()
1177 .filter(|route| route.method == method)
1178 .find_map(|route| Some((route, route.matches(path)?)))?;
1179
1180 let mut input = match body {
1181 Value::Object(fields) => fields,
1182 _ => Map::new(),
1183 };
1184 for (name, key) in route.query {
1185 if let Some((_, value)) = query.iter().find(|(query_name, _)| query_name == name) {
1186 input.insert((*key).to_owned(), Value::String(value.clone()));
1187 }
1188 }
1189 let param = |wanted: &str| {
1190 params
1191 .iter()
1192 .find(|(name, _)| *name == wanted)
1193 .map(|(_, value)| *value)
1194 };
1195 if let (Some(owner), Some(name)) = (param("owner"), param("name")) {
1196 input.insert("repo".to_owned(), Value::String(format!("{owner}/{name}")));
1197 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971198 // Every other name the path gives, under that name; the ones below that
1199 // need more (a repository, a number, an encoded name) are set after.
1200 for (key, value) in &params {
1201 if !matches!(*key, "owner" | "name") {
1202 input.insert((*key).to_owned(), Value::String(percent_decoded(value)));
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1203 }
1204 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1205 // A repository of a team's workspace, named by itself.
1206 if let (Some(workspace), Some(name)) = (param("workspace"), param("repo")) {
1207 input.insert("repo".to_owned(), Value::String(format!("{workspace}/{name}")));
1208 }
1209 // A branch name may hold slashes, sent URL-encoded as one segment, and
1210 // a label's name spaces.
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1211 if let Some(branch) = param("branch") {
1212 input.insert("branch".to_owned(), Value::String(percent_decoded(branch)));
1213 }
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971214 // An environment's name may hold slashes and spaces, URL-encoded.
1215 if let Some(environment) = param("environment") {
1216 input.insert("environment".to_owned(), Value::String(percent_decoded(environment)));
1217 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1218 if let Some(label) = param("label") {
1219 input.insert("label".to_owned(), Value::String(percent_decoded(label)));
1220 }
1221 if let Some(milestone) = param("milestone") {
1222 // Not a number: zero, which no milestone has.
1223 input.insert("milestone".to_owned(), milestone.parse::<u32>().unwrap_or(0).into());
1224 }
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1225 // GitHub says some things with the path alone.
1226 if route.path.ends_with("/enable") || route.path.ends_with("/disable") {
1227 input.insert("enabled".to_owned(), Value::Bool(route.path.ends_with("/enable")));
1228 }
1229 if route.path.ends_with("/rerun-failed-jobs") {
1230 input.insert("failed_only".to_owned(), Value::Bool(true));
1231 }
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)1232 if route.path.ends_with("/force-cancel") {
1233 input.insert("force".to_owned(), Value::Bool(true));
1234 }
API: notifications over REST and MCP, with notifications scopes1235 // Unsaving and waking a thread are a DELETE of what PUT made.
1236 if route.method == "DELETE" && route.path.ends_with("/saved") {
1237 input.insert("saved".to_owned(), Value::Bool(false));
1238 }
1239 if route.method == "DELETE" && route.path.ends_with("/snooze") {
1240 input.remove("until");
1241 }
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1242 if let Some(number) = param("number") {
1243 // Not a number: zero, which no issue or pull request has.
1244 input.insert(
1245 "number".to_owned(),
1246 number.parse::<u32>().unwrap_or(0).into(),
1247 );
1248 }
1249 Some((route, Value::Object(input)))
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254 use serde_json::json;
1255
1256 use super::*;
1257
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971258 /// Every name a route's path gives reaches the operation: a name left
1259 /// off the lists above is dropped, and the operation answers that it
1260 /// was not given (the project routes were, until this test).
1261 #[test]
1262 fn every_path_parameter_reaches_the_input() {
1263 for route in ROUTES.iter() {
1264 let names: Vec<&str> = route.path.split('/').filter_map(|part| part.strip_prefix(':')).collect();
1265 if names.is_empty() {
1266 continue;
1267 }
1268 let path: String = route
1269 .path
1270 .split('/')
1271 .map(|part| match part.strip_prefix(':') {
1272 Some("number" | "milestone") => "7".to_owned(),
1273 Some(name) => format!("{name}-x"),
1274 None => part.to_owned(),
1275 })
1276 .collect::<Vec<_>>()
1277 .join("/");
1278 let (found, input) = resolve(route.method, &path, &[], json!({})).unwrap();
1279 // A path two routes could take is checked under the first.
1280 if found.path != route.path {
1281 continue;
1282 }
1283 for name in names {
1284 let key = match name {
1285 "owner" | "name" => "repo",
1286 "repo" if names_has_workspace(route.path) => "repo",
1287 other => other,
1288 };
1289 assert!(
1290 input.get(key).is_some_and(|value| !value.is_null()),
1291 "{} {}: :{name} does not reach the input",
1292 route.method,
1293 route.path
1294 );
1295 }
1296 }
1297 }
1298
1299 fn names_has_workspace(path: &str) -> bool {
1300 path.split('/').any(|part| part == ":workspace")
1301 }
1302
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1303 #[test]
1304 fn a_path_resolves_to_its_operation_and_input() {
1305 let (route, input) = resolve(
1306 "POST",
1307 "/repos/flagon-io/hello/pulls/14/merge",
1308 &[],
1309 json!({ "keep_issue_open": true, "number": 99, "repo": "someone/else" }),
1310 )
1311 .unwrap();
1312 assert_eq!(route.op, Op::MergePullRequest);
1313 // What the path names wins over the body.
1314 assert_eq!(
1315 input,
1316 json!({ "keep_issue_open": true, "number": 14, "repo": "flagon-io/hello" })
1317 );
1318 }
1319
1320 #[test]
1321 fn a_branch_with_slashes_is_one_encoded_segment() {
1322 let (route, input) = resolve(
1323 "POST",
1324 "/repos/flagon-io/hello/branches/feature%2Flogin/rename",
1325 &[],
1326 json!({ "new_name": "feature/sign-in" }),
1327 )
1328 .unwrap();
1329 assert_eq!(route.op, Op::RenameBranch);
1330 assert_eq!(
1331 input,
1332 json!({ "new_name": "feature/sign-in", "branch": "feature/login", "repo": "flagon-io/hello" })
1333 );
1334 assert_eq!(percent_decoded("100%"), "100%");
1335 assert_eq!(percent_decoded("a%2bb%zz"), "a+b%zz");
1336 }
1337
1338 #[test]
1339 fn a_collaborator_is_named_by_username() {
1340 let (route, input) = resolve(
1341 "PATCH",
1342 "/repos/flagon-io/hello/collaborators/ada",
1343 &[],
1344 json!({ "role": "maintain" }),
1345 )
1346 .unwrap();
1347 assert_eq!(route.op, Op::UpdateCollaborator);
1348 assert_eq!(input, json!({ "role": "maintain", "username": "ada", "repo": "flagon-io/hello" }));
1349 let (route, input) = resolve("DELETE", "/user/repository_invitations/rin_1", &[], Value::Null).unwrap();
1350 assert_eq!(route.op, Op::DeclineRepoInvitation);
1351 assert_eq!(input, json!({ "id": "rin_1" }));
1352 }
1353
1354 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1355 fn teams_are_addressed_by_workspace_and_slug() {
1356 let query = [("q".to_owned(), "back".to_owned())];
1357 let (route, input) = resolve("GET", "/workspaces/acme/teams", &query, Value::Null).unwrap();
1358 assert_eq!(route.op, Op::ListTeams);
1359 assert_eq!(input, json!({ "query": "back", "workspace": "acme" }));
1360 let (route, input) = resolve("PATCH", "/workspaces/acme/teams/backend", &[], json!({ "name": "Back end" })).unwrap();
1361 assert_eq!(route.op, Op::UpdateTeam);
1362 assert_eq!(input, json!({ "name": "Back end", "workspace": "acme", "team": "backend" }));
1363 let (route, input) =
1364 resolve("PUT", "/workspaces/acme/teams/backend/members/ana", &[], json!({ "role": "maintainer" })).unwrap();
1365 assert_eq!(route.op, Op::SetTeamMember);
1366 assert_eq!(input, json!({ "role": "maintainer", "workspace": "acme", "team": "backend", "username": "ana" }));
1367 let query = [("include_child_teams".to_owned(), "true".to_owned())];
1368 let (route, input) = resolve("GET", "/workspaces/acme/teams/backend/members", &query, Value::Null).unwrap();
1369 assert_eq!(route.op, Op::ListTeamMembers);
1370 assert_eq!(input, json!({ "include_child_teams": "true", "workspace": "acme", "team": "backend" }));
1371 // A repository is named by itself, in the team's workspace.
1372 let (route, input) =
1373 resolve("PUT", "/workspaces/acme/teams/backend/repos/rocket", &[], json!({ "role": "write" })).unwrap();
1374 assert_eq!(route.op, Op::SetTeamRepo);
1375 assert_eq!(input, json!({ "role": "write", "workspace": "acme", "team": "backend", "repo": "acme/rocket" }));
1376 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1377 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/repos/rocket"), Op::RemoveTeamRepo);
1378 assert_eq!(op("GET", "/workspaces/acme/teams/backend/teams"), Op::ListChildTeams);
1379 assert_eq!(op("GET", "/workspaces/acme/teams/backend/repos"), Op::ListTeamRepos);
1380 assert_eq!(op("PUT", "/workspaces/acme/teams/backend/review_assignment"), Op::SetTeamReviewAssignment);
1381 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend"), Op::DeleteTeam);
1382 assert_eq!(op("POST", "/workspaces/acme/teams"), Op::CreateTeam);
1383 assert_eq!(op("GET", "/workspaces/acme/teams/backend"), Op::GetTeam);
1384 assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/members/ana"), Op::RemoveTeamMember);
1385 let (route, input) = resolve("GET", "/workspaces/acme/members/ana/teams", &[], Value::Null).unwrap();
1386 assert_eq!(route.op, Op::ListUserTeams);
1387 assert_eq!(input, json!({ "workspace": "acme", "username": "ana" }));
1388 }
1389
1390 #[test]
1391 fn reviewers_are_requested_and_code_owners_checked_on_a_repository() {
1392 let body = json!({ "reviewers": ["ana"], "team_reviewers": ["backend"] });
1393 let (route, input) = resolve("POST", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body.clone()).unwrap();
1394 assert_eq!(route.op, Op::RequestReviewers);
1395 assert_eq!(
1396 input,
1397 json!({ "reviewers": ["ana"], "team_reviewers": ["backend"], "repo": "acme/rocket", "number": 7 })
1398 );
1399 let (route, _) = resolve("DELETE", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body).unwrap();
1400 assert_eq!(route.op, Op::RemoveRequestedReviewers);
1401 let query = [("ref".to_owned(), "main".to_owned())];
1402 let (route, input) = resolve("GET", "/repos/acme/rocket/codeowners/errors", &query, Value::Null).unwrap();
1403 assert_eq!(route.op, Op::GetCodeownersErrors);
1404 assert_eq!(input, json!({ "ref": "main", "repo": "acme/rocket" }));
1405 }
1406
1407 #[test]
Merge branch 'worktree-agent-ad531323a21f4c975'1408 fn events_are_followed_with_a_cursor_filters_and_a_wait() {
1409 let query = [
1410 ("after".to_owned(), "evt_1".to_owned()),
1411 ("types".to_owned(), "work.claimed,integration.failed".to_owned()),
1412 ("numbers".to_owned(), "12,14".to_owned()),
1413 ("wait".to_owned(), "25".to_owned()),
1414 ("limit".to_owned(), "20".to_owned()),
1415 ("since".to_owned(), "2026-10-12T00:00:00Z".to_owned()),
1416 ];
1417 let (route, input) = resolve("GET", "/repos/acme/rocket/events", &query, Value::Null).unwrap();
1418 assert_eq!(route.op, Op::ListEvents);
1419 let asked = crate::follow::asked(&input).unwrap();
1420 assert_eq!(asked.after.as_deref(), Some("evt_1"));
1421 assert_eq!(asked.types, vec!["integration.failed", "work.claimed"]);
1422 assert_eq!(asked.numbers, vec![12, 14]);
1423 assert_eq!(asked.wait_ms, 25_000);
1424 assert_eq!(asked.limit, Some(20));
1425 assert_eq!(asked.since.as_deref(), Some("2026-10-12T00:00:00Z"));
1426 }
1427
1428 #[test]
API: notifications over REST and MCP, with notifications scopes1429 fn notifications_are_addressed_as_threads_and_by_issue() {
1430 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1", &[], Value::Null).unwrap();
1431 assert_eq!(route.op, Op::MarkThreadDone);
1432 assert_eq!(input, json!({ "id": "ntf_1" }));
1433 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/saved", &[], Value::Null).unwrap();
1434 assert_eq!(route.op, Op::SaveThread);
1435 assert_eq!(input, json!({ "id": "ntf_1", "saved": false }));
1436 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1/snooze", &[], json!({ "until": "x" })).unwrap();
1437 assert_eq!(route.op, Op::SnoozeThread);
1438 assert_eq!(input, json!({ "id": "ntf_1" }));
1439 let query = [("all".to_owned(), "true".to_owned()), ("per_page".to_owned(), "50".to_owned())];
1440 let (route, input) = resolve("GET", "/repos/acme/rocket/notifications", &query, Value::Null).unwrap();
1441 assert_eq!(route.op, Op::ListNotifications);
1442 assert_eq!(input, json!({ "all": "true", "per_page": "50", "repo": "acme/rocket" }));
1443 let (route, input) = resolve("PUT", "/repos/acme/rocket/issues/7/subscription", &[], json!({ "ignored": true })).unwrap();
1444 assert_eq!(route.op, Op::SetThreadSubscription);
1445 assert_eq!(input, json!({ "ignored": true, "number": 7, "repo": "acme/rocket" }));
1446 assert_eq!(resolve("GET", "/user/subscriptions", &[], Value::Null).unwrap().0.op, Op::ListWatchedRepos);
1447 }
1448
1449 #[test]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1450 fn labels_and_milestones_are_named_in_the_path() {
1451 let (route, input) = resolve("PATCH", "/repos/acme/web/labels/good%20first%20issue", &[], json!({ "color": "7057ff" })).unwrap();
1452 assert_eq!(route.op, Op::UpdateLabel);
1453 assert_eq!(input, json!({ "color": "7057ff", "label": "good first issue", "repo": "acme/web" }));
1454 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels/bug", &[], Value::Null).unwrap();
1455 assert_eq!(route.op, Op::RemoveIssueLabels);
1456 assert_eq!(input, json!({ "label": "bug", "number": 7, "repo": "acme/web" }));
1457 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels", &[], Value::Null).unwrap();
1458 assert_eq!(route.op, Op::RemoveIssueLabels);
1459 assert_eq!(input, json!({ "number": 7, "repo": "acme/web" }));
1460 let (route, _) = resolve("POST", "/repos/acme/web/labels/defaults", &[], Value::Null).unwrap();
1461 assert_eq!(route.op, Op::AddDefaultLabels);
1462 let (route, input) = resolve("PATCH", "/repos/acme/web/milestones/3", &[], json!({ "state": "closed" })).unwrap();
1463 assert_eq!(route.op, Op::UpdateMilestone);
1464 assert_eq!(input, json!({ "state": "closed", "milestone": 3, "repo": "acme/web" }));
1465 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "base": "release" })).unwrap();
1466 assert_eq!(route.op, Op::UpdatePullRequest);
1467 assert_eq!(input, json!({ "base": "release", "number": 9, "repo": "acme/web" }));
1468 }
1469
1470 #[test]
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1471 fn pull_requests_reopen_and_turn_draft_and_comments_are_named_by_id() {
1472 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1473 assert_eq!(op("POST", "/repos/acme/web/pulls/9/reopen"), Op::ReopenPullRequest);
1474 assert_eq!(op("POST", "/repos/acme/web/pulls/9/draft"), Op::ConvertPullRequestToDraft);
1475 assert_eq!(op("POST", "/repos/acme/web/pulls/9/close"), Op::ClosePullRequest);
1476 // Reopening and closing with a PATCH, as `state`.
1477 let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "state": "open" })).unwrap();
1478 assert_eq!(route.op, Op::UpdatePullRequest);
1479 assert_eq!(input, json!({ "state": "open", "number": 9, "repo": "acme/web" }));
1480 let (route, input) =
1481 resolve("PATCH", "/repos/acme/web/issues/comments/cmt_1", &[], json!({ "body": "Better" })).unwrap();
1482 assert_eq!(route.op, Op::EditComment);
1483 assert_eq!(input, json!({ "body": "Better", "comment_id": "cmt_1", "repo": "acme/web" }));
1484 let (route, input) = resolve("DELETE", "/repos/acme/web/issues/comments/cmt_1", &[], Value::Null).unwrap();
1485 assert_eq!(route.op, Op::DeleteComment);
1486 assert_eq!(input, json!({ "comment_id": "cmt_1", "repo": "acme/web" }));
1487 // Still an issue's comments, labels and subscription.
1488 assert_eq!(op("POST", "/repos/acme/web/issues/7/comments"), Op::AddComment);
1489 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/labels"), Op::RemoveIssueLabels);
1490 assert_eq!(op("DELETE", "/repos/acme/web/issues/7/subscription"), Op::DeleteThreadSubscription);
1491 }
1492
1493 #[test]
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.1494 fn observability_is_addressed_by_workspace_and_project() {
1495 let query = [("level".to_owned(), "error,warn".to_owned()), ("q".to_owned(), "timeout".to_owned())];
1496 let (route, input) = resolve("GET", "/workspaces/acme/projects/web/observability/logs", &query, Value::Null).unwrap();
1497 assert_eq!(route.op, Op::Telemetry(TelemetryOp::ListLogs));
1498 assert_eq!(input, json!({ "level": "error,warn", "q": "timeout", "workspace": "acme", "project": "web" }));
1499 let (route, input) =
1500 resolve("GET", "/workspaces/acme/projects/web/observability/metrics/http.server.request.duration", &[], Value::Null).unwrap();
1501 assert_eq!(route.op, Op::Telemetry(TelemetryOp::GetMetric));
1502 assert_eq!(input["metric"], json!("http.server.request.duration"));
1503 let (route, input) =
1504 resolve("PATCH", "/workspaces/acme/projects/web/observability/errors/f00d", &[], json!({ "state": "resolved" })).unwrap();
1505 assert_eq!(route.op, Op::Telemetry(TelemetryOp::UpdateError));
1506 assert_eq!(input, json!({ "state": "resolved", "workspace": "acme", "project": "web", "fingerprint": "f00d" }));
1507 }
1508
1509 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1510 fn billing_is_addressed_by_workspace() {
1511 let query = [("from".to_owned(), "2026-10-01".to_owned()), ("products".to_owned(), "agent,sandboxes".to_owned())];
1512 let (route, input) = resolve("GET", "/workspaces/acme/usage", &query, Value::Null).unwrap();
1513 assert_eq!(route.op, Op::GetUsage);
1514 assert_eq!(input, json!({ "from": "2026-10-01", "products": "agent,sandboxes", "workspace": "acme" }));
1515 let (route, input) = resolve("PUT", "/workspaces/acme/budget", &[], json!({ "alerts": [50] })).unwrap();
1516 assert_eq!(route.op, Op::SetBudget);
1517 assert_eq!(input, json!({ "alerts": [50], "workspace": "acme" }));
1518 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1519 assert_eq!(op("GET", "/workspaces/acme/budget"), Op::GetBudget);
1520 assert_eq!(op("GET", "/workspaces/acme/invoices"), Op::ListInvoices);
1521 assert_eq!(op("GET", "/workspaces/acme/billing_details"), Op::GetBillingDetails);
1522 }
1523
1524 #[test]
Merge checks: statuses and check runs on every commit1525 fn checks_are_at_githubs_addresses() {
1526 let sha = "a".repeat(40);
1527 let body = json!({ "state": "success", "context": "ci/build" });
1528 let (route, input) = resolve("POST", &format!("/repos/acme/web/statuses/{sha}"), &[], body).unwrap();
1529 assert_eq!(route.op, Op::Checks(ChecksOp::CreateCommitStatus));
1530 assert_eq!(input, json!({ "state": "success", "context": "ci/build", "sha": sha, "repo": "acme/web" }));
1531 let (route, input) = resolve("GET", "/repos/acme/web/commits/release%2F1.x/status", &[], Value::Null).unwrap();
1532 assert_eq!(route.op, Op::Checks(ChecksOp::GetCombinedStatus));
1533 assert_eq!(input, json!({ "ref": "release/1.x", "repo": "acme/web" }));
1534 let query = [("check_name".to_owned(), "lint".to_owned())];
1535 let (route, input) = resolve("GET", "/repos/acme/web/commits/main/check-runs", &query, Value::Null).unwrap();
1536 assert_eq!(route.op, Op::Checks(ChecksOp::ListCheckRunsForRef));
1537 assert_eq!(input, json!({ "check_name": "lint", "ref": "main", "repo": "acme/web" }));
1538 let (route, input) = resolve("PATCH", "/repos/acme/web/check-runs/cr_1", &[], json!({ "conclusion": "success" })).unwrap();
1539 assert_eq!(route.op, Op::Checks(ChecksOp::UpdateCheckRun));
1540 assert_eq!(input, json!({ "conclusion": "success", "id": "cr_1", "repo": "acme/web" }));
1541 let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
1542 assert_eq!(op("POST", "/repos/acme/web/check-runs"), Op::Checks(ChecksOp::CreateCheckRun));
1543 assert_eq!(op("GET", "/repos/acme/web/check-runs/cr_1/annotations"), Op::Checks(ChecksOp::ListCheckRunAnnotations));
1544 assert_eq!(op("POST", "/repos/acme/web/check-suites/cs_1/rerequest"), Op::Checks(ChecksOp::RerequestCheckSuite));
1545 assert_eq!(op("GET", "/repos/acme/web/commits/main/check-suites"), Op::Checks(ChecksOp::ListCheckSuitesForRef));
1546 }
1547
1548 #[test]
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.1549 fn archives_are_at_githubs_addresses_and_redirect() {
1550 let sha = "4807077b296e6edbf410d55e72749d3e1170c291";
1551 let (route, input) = resolve("GET", &format!("/repos/acme/web/tarball/{sha}"), &[], Value::Null).unwrap();
1552 assert_eq!(route.op, Op::Archives(ArchiveOp::DownloadTarball));
1553 assert_eq!(input, json!({ "ref": sha, "repo": "acme/web" }));
1554 // A branch with slashes, URL-encoded as octokit sends it.
1555 let (route, input) = resolve("GET", "/repos/acme/web/zipball/release%2F1.x", &[], Value::Null).unwrap();
1556 assert_eq!(route.op, Op::Archives(ArchiveOp::DownloadZipball));
1557 assert_eq!(input, json!({ "ref": "release/1.x", "repo": "acme/web" }));
1558 // Both answer with a redirect to their link, as the artifact download does.
1559 assert!(route.op.redirects());
1560 assert!(Op::Archives(ArchiveOp::DownloadTarball).redirects());
1561 assert!(Op::Artifacts(ArtifactsOp::DownloadArtifact).redirects());
1562 assert!(!Op::GetRepo.redirects());
1563 // Anyone may ask for a public repository's.
1564 assert!(!route.op.needs_user());
1565 }
1566
1567 #[test]
Merge branch 'worktree-agent-a3b9b1f730f55b5bf'1568 fn caches_are_at_githubs_addresses() {
1569 let query = [("key".to_owned(), "cargo-".to_owned()), ("ref".to_owned(), "refs/heads/main".to_owned()), ("sort".to_owned(), "size_in_bytes".to_owned())];
1570 let (route, input) = resolve("GET", "/repos/acme/web/actions/caches", &query, Value::Null).unwrap();
1571 assert_eq!(route.op, Op::Caches(CachesOp::ListCaches));
1572 assert_eq!(input, json!({ "key": "cargo-", "ref": "refs/heads/main", "sort": "size_in_bytes", "repo": "acme/web" }));
1573 let (route, _) = resolve("GET", "/repos/acme/web/actions/cache/usage", &[], Value::Null).unwrap();
1574 assert_eq!(route.op, Op::Caches(CachesOp::GetCacheUsage));
1575 // Deleting by key takes it from the query, as GitHub's does.
1576 let query = [("key".to_owned(), "npm-full-Linux-abc".to_owned())];
1577 let (route, input) = resolve("DELETE", "/repos/acme/web/actions/caches", &query, Value::Null).unwrap();
1578 assert_eq!(route.op, Op::Caches(CachesOp::DeleteCachesByKey));
1579 assert_eq!(input, json!({ "key": "npm-full-Linux-abc", "repo": "acme/web" }));
1580 assert!(!route.no_content());
1581 let (route, input) = resolve("DELETE", "/repos/acme/web/actions/caches/42", &[], Value::Null).unwrap();
1582 assert_eq!(route.op, Op::Caches(CachesOp::DeleteCache));
1583 assert_eq!(input, json!({ "id": "42", "repo": "acme/web" }));
1584 assert!(route.no_content());
1585 // Only someone signed in sees a repository's cache keys.
1586 assert!(Op::Caches(CachesOp::ListCaches).needs_user());
1587 }
1588
1589 #[test]
README: run the core forge locally with Docker Compose; forks described as copies, not copy-on-write, until Cloudflare says otherwise1590 fn query_parameters_are_renamed() {
1591 let query = [
1592 ("q".to_owned(), "parser".to_owned()),
1593 ("x".to_owned(), "y".to_owned()),
1594 ];
1595 let (route, input) = resolve("GET", "/repos", &query, Value::Null).unwrap();
1596 assert_eq!(route.op, Op::ListRepos);
1597 assert_eq!(input, json!({ "query": "parser" }));
1598 }
1599
1600 #[test]
1601 fn method_and_shape_must_match() {
1602 assert!(resolve("GET", "/repos/a/b/issues/1/close", &[], Value::Null).is_none());
1603 assert!(resolve("GET", "/repos/a", &[], Value::Null).is_none());
1604 assert!(resolve("GET", "/repos/a/b/issues/1/extra", &[], Value::Null).is_none());
1605 assert!(resolve("GET", "/repos/a/b/", &[], Value::Null).is_some());
1606 }
1607
1608 #[test]
1609 fn every_parameter_and_query_name_is_an_input() {
1610 for route in ROUTES {
1611 let properties = route.op.properties();
1612 for (_, key) in route.query {
1613 assert!(properties.contains_key(*key), "{}: {key}", route.path);
1614 }
1615 for name in route.params() {
1616 let covered = matches!(name, "owner" | "name") && properties.contains_key("repo")
1617 || properties.contains_key(name);
1618 assert!(covered, "{}: {name}", route.path);
1619 }
1620 }
1621 }
1622
1623 #[test]
1624 fn every_operation_has_a_route() {
1625 for op in Op::ALL {
1626 assert!(ROUTES.iter().any(|route| route.op == op), "{}", op.name());
1627 }
1628 }
1629}

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