Skip to content
228 linesCodeBlameRaw

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

Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1/**
2 * Rulesets on the site: how each rule is shown and set up, a new ruleset
3 * and rule with their defaults, export and import as JSON, and how people
4 * read who may bypass a ruleset. The types are `@g1t/contracts`' rules.ts.
5 */
6import type {
7 AppliesTo,
8 BypassActor,
9 Conditions,
10 Enforcement,
11 Level,
12 PatternOperator,
13 PatternParameters,
14 Rule,
15 RuleEntry,
16 RuleType,
17 RulesetSpec,
18 Target,
19} from "@g1t/contracts";
20
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.21/**
22 * Rule types no longer offered (contracts' RETIRED_RULES): an import that
23 * has one leaves it out, as the service does with a stored ruleset.
24 */
25const RETIRED_RULES: readonly string[] = ["confidence_threshold", "cost_cap"];
26
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge27/** The default branch, whatever it is called (contracts' DEFAULT_BRANCH). */
28export const DEFAULT_BRANCH = "~DEFAULT_BRANCH";
29/** Every branch, tag or repository (contracts' ALL). */
30export const ALL = "~ALL";
31
32/** How each rule is shown and set up on the site. */
33export type RuleTypeInfo = {
34 type: RuleType;
35 label: string;
36 /** What it does, in a sentence. */
37 about: string;
38 group: "branches" | "pull_requests" | "commits" | "files" | "agents";
39 /** Whether it can target tags, branches or both. */
40 targets: Target[];
41 /** Parameters a new rule starts with. */
42 defaults: Rule["parameters"];
43 /** Rules that may appear more than once. */
44 repeatable?: boolean;
45};
46
47const none = {} as Record<string, never>;
48const pattern = (operator: PatternOperator = "regex"): PatternParameters => ({ name: "", operator, pattern: "", negate: false });
49
50export const RULES: RuleTypeInfo[] = [
51 { type: "creation", label: "Restrict creations", about: "Only bypass actors may create matching branches or tags.", group: "branches", targets: ["branch", "tag"], defaults: none },
52 { type: "update", label: "Restrict updates", about: "Only bypass actors may push to matching branches or tags, merges included.", group: "branches", targets: ["branch", "tag"], defaults: none },
53 { type: "deletion", label: "Restrict deletions", about: "Only bypass actors may delete matching branches or tags.", group: "branches", targets: ["branch", "tag"], defaults: none },
54 { type: "non_fast_forward", label: "Block force pushes", about: "Nobody rewrites history: a push must only add to it.", group: "branches", targets: ["branch", "tag"], defaults: none },
55 { type: "required_linear_history", label: "Require linear history", about: "No merge commits: rebase instead of merging the branch in.", group: "commits", targets: ["branch", "tag"], defaults: none },
56 { type: "required_signatures", label: "Require signed commits", about: "Every commit carries an SSH signature g1t verifies against its committer's account.", group: "commits", targets: ["branch", "tag"], defaults: none },
57 {
58 type: "pull_request",
59 label: "Require a pull request before merging",
60 about: "Changes arrive only through pull requests, with the reviews set here.",
61 group: "pull_requests",
62 targets: ["branch"],
63 defaults: {
64 required_approvals: 1,
65 count_agent_approvals: true,
66 dismiss_stale_reviews_on_push: false,
67 require_code_owner_review: false,
68 require_last_push_approval: false,
69 allowed_merge_methods: [],
70 },
71 },
72 {
73 type: "required_status_checks",
74 label: "Require status checks to pass",
75 about: "Checks that must pass on a pull request's head before it merges, for every file or only some paths.",
76 group: "pull_requests",
77 targets: ["branch"],
78 defaults: { checks: [], strict: false, paths: [], allow_bypass_on_merge: false },
79 repeatable: true,
80 },
81 {
82 type: "merge_queue",
83 label: "Require the merge queue",
84 about: "Merging into the default branch joins the queue, which tests pull requests together before it moves.",
85 group: "pull_requests",
86 targets: ["branch"],
87 defaults: { merge_method: "merge", max_entries_to_build: 4, min_entries_to_merge: 1, min_entries_wait_minutes: 0, check_response_timeout_minutes: 45 },
88 },
89 { type: "required_deployments", label: "Require deployments to succeed", about: "A pull request's head must have deployed to these environments.", group: "pull_requests", targets: ["branch"], defaults: { environments: ["preview"] } },
90 { type: "commit_message_pattern", label: "Commit message pattern", about: "Every commit message must match, or must not.", group: "commits", targets: ["branch", "tag"], defaults: pattern(), repeatable: true },
91 { type: "commit_author_email_pattern", label: "Commit author email pattern", about: "Every author address must match, or must not.", group: "commits", targets: ["branch", "tag"], defaults: pattern("ends_with"), repeatable: true },
92 { type: "committer_email_pattern", label: "Committer email pattern", about: "Every committer address must match, or must not.", group: "commits", targets: ["branch", "tag"], defaults: pattern("ends_with"), repeatable: true },
93 { type: "branch_name_pattern", label: "Branch name pattern", about: "New branches must be named to match, or not to.", group: "branches", targets: ["branch"], defaults: pattern(), repeatable: true },
94 { type: "tag_name_pattern", label: "Tag name pattern", about: "New tags must be named to match, or not to.", group: "branches", targets: ["tag"], defaults: pattern(), repeatable: true },
95 { type: "file_path_restriction", label: "Restrict file paths", about: "Changes to matching paths are refused.", group: "files", targets: ["branch", "tag"], defaults: { restricted_file_paths: [] }, repeatable: true },
96 { type: "file_extension_restriction", label: "Restrict file extensions", about: "Files with these extensions may not be added.", group: "files", targets: ["branch", "tag"], defaults: { restricted_file_extensions: [] } },
97 { type: "max_file_size", label: "Restrict file size", about: "No file larger than this.", group: "files", targets: ["branch", "tag"], defaults: { max_file_size_mb: 10 } },
98 { type: "max_file_path_length", label: "Restrict file path length", about: "No path longer than this.", group: "files", targets: ["branch", "tag"], defaults: { max_file_path_length: 255 } },
99 { type: "max_files_changed", label: "Restrict files changed", about: "A commit may change at most this many files.", group: "files", targets: ["branch", "tag"], defaults: { max_files: 100 } },
100 { type: "secret_scanning", label: "Block pushes that add secrets", about: "Every push is scanned; one too large to scan is refused instead of let through.", group: "files", targets: ["branch", "tag"], defaults: none },
101 { type: "path_review", label: "Review for sensitive paths", about: "Changes to these paths need approvals from people, from a team if you name one.", group: "agents", targets: ["branch"], defaults: { paths: [], required_approvals: 1, team: null }, repeatable: true },
102 { type: "merge_window", label: "Merge window", about: "When pull requests may merge: weekly hours, freezes and exceptions.", group: "agents", targets: ["branch"], defaults: { time_zone: "UTC", windows: [], freezes: [], exceptions: [] } },
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.103 { type: "agent_auto_merge", label: "Agent auto-merge", about: "Whether an agent's ready change lands here without a person.", group: "agents", targets: ["branch"], defaults: { allowed: true } },
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge104];
105
106export const RULE_GROUPS: { id: RuleTypeInfo["group"]; label: string }[] = [
107 { id: "branches", label: "Branches and tags" },
108 { id: "pull_requests", label: "Pull requests and checks" },
109 { id: "commits", label: "Commits" },
110 { id: "files", label: "Files" },
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.111 { id: "agents", label: "Review, timing and auto-merge" },
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge112];
113
114/** A rule type's information, or undefined for an unknown type. */
115export function ruleInfo(type: string): RuleTypeInfo | undefined {
116 return RULES.find((rule) => rule.type === type);
117}
118
119/** A new ruleset, as the form starts it. */
120export function newRuleset(level: Level): RulesetSpec {
121 return {
122 name: "",
123 enforcement: "active",
124 target: "branch",
125 conditions: {
126 ref_name: { include: [DEFAULT_BRANCH], exclude: [] },
127 ...(level === "workspace" ? { repository: { include: [ALL], exclude: [], visibility: "any" as const, topics: [] } } : {}),
128 },
129 bypass_actors: [],
130 rules: [],
131 };
132}
133
134/** A new rule of a type, with its defaults. */
135export function newRule(type: RuleType, appliesTo: AppliesTo = "everyone"): RuleEntry {
136 const info = ruleInfo(type);
137 return { type, parameters: structuredClone(info?.defaults ?? {}), applies_to: appliesTo } as RuleEntry;
138}
139
140/** What a ruleset exports as: its spec, nothing about where it was kept. */
141export function exportRuleset(ruleset: RulesetSpec): RulesetSpec {
142 const { name, enforcement, target, conditions, bypass_actors, rules } = ruleset;
143 return { name, enforcement, target, conditions, bypass_actors, rules };
144}
145
146const ENFORCEMENTS: Enforcement[] = ["active", "evaluate", "disabled"];
147const APPLIES: AppliesTo[] = ["everyone", "agents", "people"];
148
149/**
150 * A ruleset from imported JSON: an exported one, or one as the API shows it
151 * (`ruleset_name` read as its name). Parameters left out take their
152 * defaults. Throws with what is wrong.
153 */
154export function importRuleset(text: string, level: Level): RulesetSpec {
155 let raw: unknown;
156 try {
157 raw = JSON.parse(text);
158 } catch {
159 throw new Error("That is not JSON.");
160 }
161 if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("A ruleset is one JSON object.");
162 const found = raw as Record<string, unknown>;
163 const base = newRuleset(level);
164 const name = typeof found.name === "string" ? found.name : typeof found.ruleset_name === "string" ? found.ruleset_name : "";
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.165 // Rule types no longer offered are left out, as the service does.
166 const rules = (Array.isArray(found.rules) ? (found.rules as Record<string, unknown>[]) : []).filter(
167 (rule) => !(typeof rule?.type === "string" && RETIRED_RULES.includes(rule.type)),
168 );
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge169 for (const rule of rules) {
170 if (typeof rule?.type !== "string" || !ruleInfo(rule.type)) throw new Error(`${String(rule?.type)} is not a rule type.`);
171 }
172 const conditions = (found.conditions as Partial<Conditions> | undefined) ?? base.conditions;
173 return {
174 name,
175 enforcement: ENFORCEMENTS.includes(found.enforcement as Enforcement) ? (found.enforcement as Enforcement) : "active",
176 target: found.target === "tag" ? "tag" : "branch",
177 conditions: {
178 ref_name: { include: conditions.ref_name?.include ?? [], exclude: conditions.ref_name?.exclude ?? [] },
179 ...(level === "workspace" ? { repository: conditions.repository ?? base.conditions.repository } : {}),
180 },
181 bypass_actors: Array.isArray(found.bypass_actors) ? (found.bypass_actors as BypassActor[]) : [],
182 rules: rules.map((rule) => {
183 const info = ruleInfo(rule.type as string)!;
184 return {
185 type: info.type,
186 parameters: { ...structuredClone(info.defaults), ...((rule.parameters as object) ?? {}) },
187 applies_to: APPLIES.includes(rule.applies_to as AppliesTo) ? (rule.applies_to as AppliesTo) : "everyone",
188 } as RuleEntry;
189 }),
190 };
191}
192
193/** How people read who a bypass actor is. */
194export function describeBypassActor(actor: BypassActor): string {
195 switch (actor.kind) {
196 case "g1t":
197 return "g1t";
198 case "role":
199 return actor.value === "owner" ? "Workspace owners" : `${actor.value[0]?.toUpperCase() ?? ""}${actor.value.slice(1)} role and up`;
200 case "team":
201 case "user":
202 return `@${actor.value}`;
203 case "token":
204 return actor.value === "workspace" ? "The workspace's tokens" : `Token ${actor.value}`;
205 }
206}
207
208/** How people read whose changes a rule holds for. */
209export function describeAppliesTo(appliesTo: AppliesTo): string {
210 return appliesTo === "agents" ? "Agents' changes" : appliesTo === "people" ? "People's changes" : "Everyone";
211}
212
213function patternLabel(pattern: string): string {
214 if (pattern === DEFAULT_BRANCH) return "Default branch";
215 if (pattern === ALL) return "All";
216 return pattern;
217}
218
219/** A name pattern as people read it: `~DEFAULT_BRANCH` is "Default branch". */
220export { patternLabel };
221
222/** What a ruleset targets, in a few words: "Default branch, release/*". */
223export function targetSummary(ruleset: Pick<RulesetSpec, "conditions">): string {
224 const include = ruleset.conditions.ref_name.include.map(patternLabel);
225 const exclude = ruleset.conditions.ref_name.exclude.map(patternLabel);
226 const what = include.length === 0 ? "Nothing yet" : include.join(", ");
227 return exclude.length ? `${what}, except ${exclude.join(", ")}` : what;
228}

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