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.
| API and MCP server, Rust identity service, registration, site redesign | 1 | import { |
| 2 | type EventsApi, | |
| Rust repos service with shipping; pull requests kept in the model | 3 | type IdentityApi, |
| API and MCP server, Rust identity service, registration, site redesign | 4 | type NewSessionEntry, |
| 5 | type RepoPath, | |
| 6 | type ReposApi, | |
| 7 | type Result, | |
| 8 | type User, | |
| 9 | type Viewer, | |
| 10 | type WorkApi, | |
| 11 | fail, | |
| 12 | ok, | |
| 13 | } from "@g1t/contracts"; | |
| 14 | ||
| 15 | export interface ApiEnv { | |
| Rust repos service with shipping; pull requests kept in the model | 16 | IDENTITY: IdentityApi; |
| API and MCP server, Rust identity service, registration, site redesign | 17 | REPOS: ReposApi; |
| 18 | WORK: WorkApi; | |
| 19 | EVENTS: EventsApi; | |
| 20 | } | |
| 21 | ||
| 22 | type Input = Record<string, unknown>; | |
| 23 | ||
| 24 | type JsonSchema = { | |
| 25 | type: "object"; | |
| 26 | properties: Record<string, object>; | |
| 27 | required?: string[]; | |
| 28 | }; | |
| 29 | ||
| 30 | /** | |
| 31 | * One thing a client can do. REST routes and MCP tools are both generated | |
| 32 | * from this list, so the two surfaces cannot drift apart. | |
| 33 | */ | |
| 34 | export type Operation = { | |
| 35 | name: string; | |
| 36 | description: string; | |
| 37 | input: JsonSchema; | |
| 38 | run(env: ApiEnv, viewer: Viewer, input: Input): Promise<Result<unknown>>; | |
| 39 | }; | |
| 40 | ||
| 41 | const SIGN_IN = fail("unauthenticated", "This needs a g1t access token."); | |
| 42 | ||
| 43 | const REPO = { | |
| 44 | type: "string", | |
| 45 | description: 'Repository as "owner/name", e.g. "syntaqx/hello".', | |
| 46 | }; | |
| 47 | ||
| 48 | function text(input: Input, key: string): string { | |
| 49 | const value = input[key]; | |
| 50 | return typeof value === "string" ? value : ""; | |
| 51 | } | |
| 52 | ||
| 53 | function repoPath(input: Input): RepoPath | null { | |
| 54 | const [namespace, name, ...rest] = text(input, "repo").split("/"); | |
| 55 | return namespace && name && rest.length === 0 ? { namespace, name } : null; | |
| 56 | } | |
| 57 | ||
| 58 | const BAD_REPO = fail("invalid", 'Give the repository as "owner/name".'); | |
| 59 | ||
| 60 | /** Wraps an operation that needs a signed-in user. */ | |
| 61 | function authed( | |
| 62 | run: (env: ApiEnv, user: User, input: Input) => Promise<Result<unknown>>, | |
| 63 | ): Operation["run"] { | |
| 64 | return (env, viewer, input) => | |
| 65 | viewer ? run(env, viewer, input) : Promise.resolve(SIGN_IN); | |
| 66 | } | |
| 67 | ||
| 68 | export const operations: Operation[] = [ | |
| 69 | { | |
| 70 | name: "whoami", | |
| Workspaces own repositories | 71 | description: "The account the access token belongs to, and its workspaces.", |
| API and MCP server, Rust identity service, registration, site redesign | 72 | input: { type: "object", properties: {} }, |
| 73 | run: authed(async (_env, user) => ok(user)), | |
| 74 | }, | |
| 75 | { | |
| Workspaces own repositories | 76 | name: "create_workspace", |
| 77 | description: | |
| 78 | "Create a workspace. A workspace owns repositories and is the first part of their address, g1t.sh/<workspace>/<repo>. The whoami tool lists the ones you already belong to.", | |
| 79 | input: { | |
| 80 | type: "object", | |
| 81 | properties: { | |
| 82 | slug: { | |
| 83 | type: "string", | |
| 84 | description: "Its name in URLs: lowercase letters, digits and single hyphens.", | |
| 85 | }, | |
| 86 | name: { type: "string", description: "A display name." }, | |
| 87 | }, | |
| 88 | required: ["slug"], | |
| 89 | }, | |
| 90 | run: authed((env, user, input) => | |
| 91 | env.IDENTITY.createWorkspace(user, text(input, "slug"), text(input, "name")), | |
| 92 | ), | |
| 93 | }, | |
| 94 | { | |
| API and MCP server, Rust identity service, registration, site redesign | 95 | name: "list_repos", |
| 96 | description: "Repositories you can see, optionally filtered by a search query.", | |
| 97 | input: { | |
| 98 | type: "object", | |
| 99 | properties: { query: { type: "string", description: "Matches name or description." } }, | |
| 100 | }, | |
| 101 | run: async (env, viewer, input) => | |
| 102 | ok(await env.REPOS.list(viewer, { query: text(input, "query") })), | |
| 103 | }, | |
| 104 | { | |
| 105 | name: "get_repo", | |
| 106 | description: "One repository's details.", | |
| 107 | input: { type: "object", properties: { repo: REPO }, required: ["repo"] }, | |
| 108 | run: async (env, viewer, input) => { | |
| 109 | const path = repoPath(input); | |
| 110 | return path ? env.REPOS.get(path, viewer) : BAD_REPO; | |
| 111 | }, | |
| 112 | }, | |
| 113 | { | |
| 114 | name: "create_repo", | |
| Workspaces own repositories | 115 | description: "Create a repository in one of your workspaces.", |
| API and MCP server, Rust identity service, registration, site redesign | 116 | input: { |
| 117 | type: "object", | |
| 118 | properties: { | |
| Workspaces own repositories | 119 | workspace: { |
| 120 | type: "string", | |
| 121 | description: | |
| 122 | "The workspace to create it in. May be left out if you belong to exactly one.", | |
| 123 | }, | |
| API and MCP server, Rust identity service, registration, site redesign | 124 | name: { type: "string" }, |
| 125 | description: { type: "string" }, | |
| 126 | private: { type: "boolean" }, | |
| 127 | }, | |
| 128 | required: ["name"], | |
| 129 | }, | |
| 130 | run: authed((env, user, input) => | |
| 131 | env.REPOS.create(user, { | |
| Workspaces own repositories | 132 | namespace: |
| 133 | text(input, "workspace") || | |
| 134 | (user.workspaces?.length === 1 ? user.workspaces[0].slug : ""), | |
| API and MCP server, Rust identity service, registration, site redesign | 135 | name: text(input, "name"), |
| 136 | description: text(input, "description"), | |
| 137 | isPrivate: input.private === true, | |
| 138 | }), | |
| 139 | ), | |
| 140 | }, | |
| 141 | { | |
| 142 | name: "list_intents", | |
| 143 | description: | |
| 144 | "Intents on a repository. An intent is a goal that agents attempt in parallel; it replaces issues and pull requests.", | |
| 145 | input: { | |
| 146 | type: "object", | |
| 147 | properties: { | |
| 148 | repo: REPO, | |
| 149 | status: { type: "string", enum: ["open", "shipped", "withdrawn"] }, | |
| 150 | }, | |
| 151 | required: ["repo"], | |
| 152 | }, | |
| 153 | run: async (env, viewer, input) => { | |
| 154 | const path = repoPath(input); | |
| 155 | const status = text(input, "status"); | |
| 156 | if (!path) return BAD_REPO; | |
| 157 | return env.WORK.listIntents( | |
| 158 | path, | |
| 159 | viewer, | |
| 160 | status === "open" || status === "shipped" || status === "withdrawn" | |
| 161 | ? status | |
| 162 | : undefined, | |
| 163 | ); | |
| 164 | }, | |
| 165 | }, | |
| 166 | { | |
| 167 | name: "get_intent", | |
| 168 | description: | |
| 169 | "An intent's brief, its acceptance checks, and every attempt made at it so far. Read this before starting an attempt.", | |
| 170 | input: { | |
| 171 | type: "object", | |
| 172 | properties: { repo: REPO, number: { type: "integer" } }, | |
| 173 | required: ["repo", "number"], | |
| 174 | }, | |
| 175 | run: async (env, viewer, input) => { | |
| 176 | const path = repoPath(input); | |
| 177 | return path | |
| 178 | ? env.WORK.getIntent(path, Number(input.number), viewer) | |
| 179 | : BAD_REPO; | |
| 180 | }, | |
| 181 | }, | |
| 182 | { | |
| 183 | name: "open_intent", | |
| 184 | description: "State a new goal for a repository.", | |
| 185 | input: { | |
| 186 | type: "object", | |
| 187 | properties: { | |
| 188 | repo: REPO, | |
| 189 | title: { type: "string", description: "The goal in one line." }, | |
| 190 | brief: { | |
| 191 | type: "string", | |
| 192 | description: "What an agent needs to do the work: outcome, constraints, context.", | |
| 193 | }, | |
| 194 | checks: { | |
| 195 | type: "array", | |
| 196 | items: { type: "string" }, | |
| 197 | description: "Commands that must pass for an attempt to be accepted.", | |
| 198 | }, | |
| 199 | }, | |
| 200 | required: ["repo", "title", "brief"], | |
| 201 | }, | |
| 202 | run: authed(async (env, user, input) => { | |
| 203 | const path = repoPath(input); | |
| 204 | if (!path) return BAD_REPO; | |
| 205 | return env.WORK.openIntent(user, path, { | |
| 206 | title: text(input, "title"), | |
| 207 | brief: text(input, "brief"), | |
| 208 | checks: Array.isArray(input.checks) ? input.checks.map(String) : [], | |
| 209 | }); | |
| 210 | }), | |
| 211 | }, | |
| 212 | { | |
| 213 | name: "start_attempt", | |
| 214 | description: | |
| 215 | "Begin working on an intent. Creates a private fork for this attempt and returns its git remote. Clone it, commit your work there, push, record your session as you go, then call submit_attempt.", | |
| 216 | input: { | |
| 217 | type: "object", | |
| 218 | properties: { | |
| 219 | intent_id: { type: "string" }, | |
| 220 | agent: { | |
| 221 | type: "string", | |
| 222 | description: 'A label for the agent doing the work, e.g. "claude-code".', | |
| 223 | }, | |
| 224 | }, | |
| 225 | required: ["intent_id"], | |
| 226 | }, | |
| 227 | run: authed(async (env, user, input) => { | |
| 228 | const started = await env.WORK.startAttempt(user, text(input, "intent_id"), { | |
| 229 | agent: text(input, "agent") || "agent", | |
| 230 | runtime: "external", | |
| 231 | }); | |
| 232 | if (!started.ok) return started; | |
| 233 | const { fork } = started.value; | |
| 234 | return ok({ | |
| 235 | attempt: started.value, | |
| 236 | git: { | |
| 237 | remote: `https://g1t.sh/${fork.namespace}/${fork.name}.git`, | |
| 238 | username: user.username, | |
| 239 | password: "your g1t access token", | |
| 240 | }, | |
| 241 | }); | |
| 242 | }), | |
| 243 | }, | |
| 244 | { | |
| 245 | name: "get_attempt", | |
| 246 | description: "An attempt's status, head commit and the intent it belongs to.", | |
| 247 | input: { | |
| 248 | type: "object", | |
| 249 | properties: { attempt_id: { type: "string" } }, | |
| 250 | required: ["attempt_id"], | |
| 251 | }, | |
| 252 | run: (env, viewer, input) => | |
| 253 | env.WORK.getAttempt(text(input, "attempt_id"), viewer), | |
| 254 | }, | |
| 255 | { | |
| 256 | name: "record_session", | |
| 257 | description: | |
| 258 | "Append entries to an attempt's session: the prompt you were given, your reasoning, the tools you ran. This is how people later see why a change was made, so record as you work, not only at the end.", | |
| 259 | input: { | |
| 260 | type: "object", | |
| 261 | properties: { | |
| 262 | attempt_id: { type: "string" }, | |
| 263 | entries: { | |
| 264 | type: "array", | |
| 265 | items: { | |
| 266 | type: "object", | |
| 267 | properties: { | |
| 268 | kind: { | |
| 269 | type: "string", | |
| 270 | enum: ["prompt", "message", "tool_call", "tool_result", "note"], | |
| 271 | }, | |
| 272 | text: { type: "string" }, | |
| 273 | tool: { type: "string", description: "Tool name, for tool entries." }, | |
| 274 | }, | |
| 275 | required: ["kind", "text"], | |
| 276 | }, | |
| 277 | }, | |
| 278 | }, | |
| 279 | required: ["attempt_id", "entries"], | |
| 280 | }, | |
| 281 | run: authed(async (env, user, input) => { | |
| 282 | if (!Array.isArray(input.entries)) { | |
| 283 | return fail("invalid", "entries must be an array."); | |
| 284 | } | |
| 285 | return env.WORK.appendSession( | |
| 286 | user, | |
| 287 | text(input, "attempt_id"), | |
| 288 | input.entries as NewSessionEntry[], | |
| 289 | ); | |
| 290 | }), | |
| 291 | }, | |
| 292 | { | |
| 293 | name: "read_session", | |
| 294 | description: "The recorded session of an attempt, oldest entry first.", | |
| 295 | input: { | |
| 296 | type: "object", | |
| 297 | properties: { | |
| 298 | attempt_id: { type: "string" }, | |
| 299 | after: { type: "integer", description: "Only entries after this sequence number." }, | |
| 300 | }, | |
| 301 | required: ["attempt_id"], | |
| 302 | }, | |
| 303 | run: (env, viewer, input) => | |
| 304 | env.WORK.readSession( | |
| 305 | text(input, "attempt_id"), | |
| 306 | viewer, | |
| 307 | Number(input.after) || 0, | |
| 308 | ), | |
| 309 | }, | |
| 310 | { | |
| 311 | name: "submit_attempt", | |
| 312 | description: | |
| 313 | "Mark an attempt ready to be compared and shipped. Push your commits first. The summary should say what changed and why.", | |
| 314 | input: { | |
| 315 | type: "object", | |
| 316 | properties: { attempt_id: { type: "string" }, summary: { type: "string" } }, | |
| 317 | required: ["attempt_id", "summary"], | |
| 318 | }, | |
| 319 | run: authed((env, user, input) => | |
| 320 | env.WORK.submitAttempt(user, text(input, "attempt_id"), text(input, "summary")), | |
| 321 | ), | |
| 322 | }, | |
| 323 | { | |
| 324 | name: "abandon_attempt", | |
| 325 | description: "Give up on an attempt.", | |
| 326 | input: { | |
| 327 | type: "object", | |
| 328 | properties: { attempt_id: { type: "string" } }, | |
| 329 | required: ["attempt_id"], | |
| 330 | }, | |
| 331 | run: authed((env, user, input) => | |
| 332 | env.WORK.abandonAttempt(user, text(input, "attempt_id")), | |
| 333 | ), | |
| 334 | }, | |
| 335 | { | |
| docs.g1t.sh, generated OpenAPI with an interactive reference, full footer | 336 | name: "get_attempt_changes", |
| 337 | description: | |
| 338 | "What an attempt changed: the files it touched and their line-by-line diff against the commit it started from. Use it to review an attempt or to compare several attempts at the same intent.", | |
| 339 | input: { | |
| 340 | type: "object", | |
| 341 | properties: { attempt_id: { type: "string" } }, | |
| 342 | required: ["attempt_id"], | |
| 343 | }, | |
| 344 | run: async (env, viewer, input) => { | |
| 345 | const found = await env.WORK.getAttempt(text(input, "attempt_id"), viewer); | |
| 346 | if (!found.ok) return found; | |
| 347 | const { forkRepoId, landedBase } = found.value.attempt; | |
| 348 | return env.REPOS.compare(forkRepoId, viewer, landedBase); | |
| 349 | }, | |
| 350 | }, | |
| 351 | { | |
| Rust repos service with shipping; pull requests kept in the model | 352 | name: "ship_attempt", |
| 353 | description: | |
| 354 | "Land an attempt on the repository's main branch and close its intent. Only the repository's owner can ship. Fails if main has moved since the attempt started; the attempt must then pull main into its fork and push before shipping again.", | |
| 355 | input: { | |
| 356 | type: "object", | |
| 357 | properties: { attempt_id: { type: "string" } }, | |
| 358 | required: ["attempt_id"], | |
| 359 | }, | |
| 360 | run: authed((env, user, input) => | |
| 361 | env.WORK.shipAttempt(user, text(input, "attempt_id")), | |
| 362 | ), | |
| 363 | }, | |
| 364 | { | |
| API and MCP server, Rust identity service, registration, site redesign | 365 | name: "list_events", |
| 366 | description: | |
| 367 | "The timeline of a repository: pushes, intents, attempts and session activity, newest first.", | |
| 368 | input: { | |
| 369 | type: "object", | |
| 370 | properties: { | |
| 371 | repo: REPO, | |
| 372 | before: { type: "string", description: "Event id to page back from." }, | |
| 373 | }, | |
| 374 | required: ["repo"], | |
| 375 | }, | |
| 376 | run: async (env, viewer, input) => { | |
| 377 | const path = repoPath(input); | |
| 378 | if (!path) return BAD_REPO; | |
| 379 | const repo = await env.REPOS.get(path, viewer); | |
| 380 | if (!repo.ok) return repo; | |
| 381 | return ok( | |
| 382 | await env.EVENTS.list({ | |
| 383 | repoId: repo.value.id, | |
| 384 | before: text(input, "before") || undefined, | |
| 385 | }), | |
| 386 | ); | |
| 387 | }, | |
| 388 | }, | |
| 389 | ]; | |
| 390 | ||
| 391 | export const operationsByName = new Map( | |
| 392 | operations.map((operation) => [operation.name, operation]), | |
| 393 | ); |