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 merge | 1 | /** |
| 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 | */ | |
| 6 | import 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 | */ | |
| 25 | const RETIRED_RULES: readonly string[] = ["confidence_threshold", "cost_cap"]; | |
| 26 | ||
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 27 | /** The default branch, whatever it is called (contracts' DEFAULT_BRANCH). */ |
| 28 | export const DEFAULT_BRANCH = "~DEFAULT_BRANCH"; | |
| 29 | /** Every branch, tag or repository (contracts' ALL). */ | |
| 30 | export const ALL = "~ALL"; | |
| 31 | ||
| 32 | /** How each rule is shown and set up on the site. */ | |
| 33 | export 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 | ||
| 47 | const none = {} as Record<string, never>; | |
| 48 | const pattern = (operator: PatternOperator = "regex"): PatternParameters => ({ name: "", operator, pattern: "", negate: false }); | |
| 49 | ||
| 50 | export 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 merge | 104 | ]; |
| 105 | ||
| 106 | export 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 merge | 112 | ]; |
| 113 | ||
| 114 | /** A rule type's information, or undefined for an unknown type. */ | |
| 115 | export 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. */ | |
| 120 | export 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. */ | |
| 135 | export 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. */ | |
| 141 | export 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 | ||
| 146 | const ENFORCEMENTS: Enforcement[] = ["active", "evaluate", "disabled"]; | |
| 147 | const 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 | */ | |
| 154 | export 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 merge | 169 | 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. */ | |
| 194 | export 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. */ | |
| 209 | export function describeAppliesTo(appliesTo: AppliesTo): string { | |
| 210 | return appliesTo === "agents" ? "Agents' changes" : appliesTo === "people" ? "People's changes" : "Everyone"; | |
| 211 | } | |
| 212 | ||
| 213 | function 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". */ | |
| 220 | export { patternLabel }; | |
| 221 | ||
| 222 | /** What a ruleset targets, in a few words: "Default branch, release/*". */ | |
| 223 | export 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.