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", | |
| 71 | description: "The account the access token belongs to.", | |
| 72 | input: { type: "object", properties: {} }, | |
| 73 | run: authed(async (_env, user) => ok(user)), | |
| 74 | }, | |
| 75 | { | |
| 76 | name: "list_repos", | |
| 77 | description: "Repositories you can see, optionally filtered by a search query.", | |
| 78 | input: { | |
| 79 | type: "object", | |
| 80 | properties: { query: { type: "string", description: "Matches name or description." } }, | |
| 81 | }, | |
| 82 | run: async (env, viewer, input) => | |
| 83 | ok(await env.REPOS.list(viewer, { query: text(input, "query") })), | |
| 84 | }, | |
| 85 | { | |
| 86 | name: "get_repo", | |
| 87 | description: "One repository's details.", | |
| 88 | input: { type: "object", properties: { repo: REPO }, required: ["repo"] }, | |
| 89 | run: async (env, viewer, input) => { | |
| 90 | const path = repoPath(input); | |
| 91 | return path ? env.REPOS.get(path, viewer) : BAD_REPO; | |
| 92 | }, | |
| 93 | }, | |
| 94 | { | |
| 95 | name: "create_repo", | |
| 96 | description: "Create a repository under your account.", | |
| 97 | input: { | |
| 98 | type: "object", | |
| 99 | properties: { | |
| 100 | name: { type: "string" }, | |
| 101 | description: { type: "string" }, | |
| 102 | private: { type: "boolean" }, | |
| 103 | }, | |
| 104 | required: ["name"], | |
| 105 | }, | |
| 106 | run: authed((env, user, input) => | |
| 107 | env.REPOS.create(user, { | |
| 108 | name: text(input, "name"), | |
| 109 | description: text(input, "description"), | |
| 110 | isPrivate: input.private === true, | |
| 111 | }), | |
| 112 | ), | |
| 113 | }, | |
| 114 | { | |
| 115 | name: "list_intents", | |
| 116 | description: | |
| 117 | "Intents on a repository. An intent is a goal that agents attempt in parallel; it replaces issues and pull requests.", | |
| 118 | input: { | |
| 119 | type: "object", | |
| 120 | properties: { | |
| 121 | repo: REPO, | |
| 122 | status: { type: "string", enum: ["open", "shipped", "withdrawn"] }, | |
| 123 | }, | |
| 124 | required: ["repo"], | |
| 125 | }, | |
| 126 | run: async (env, viewer, input) => { | |
| 127 | const path = repoPath(input); | |
| 128 | const status = text(input, "status"); | |
| 129 | if (!path) return BAD_REPO; | |
| 130 | return env.WORK.listIntents( | |
| 131 | path, | |
| 132 | viewer, | |
| 133 | status === "open" || status === "shipped" || status === "withdrawn" | |
| 134 | ? status | |
| 135 | : undefined, | |
| 136 | ); | |
| 137 | }, | |
| 138 | }, | |
| 139 | { | |
| 140 | name: "get_intent", | |
| 141 | description: | |
| 142 | "An intent's brief, its acceptance checks, and every attempt made at it so far. Read this before starting an attempt.", | |
| 143 | input: { | |
| 144 | type: "object", | |
| 145 | properties: { repo: REPO, number: { type: "integer" } }, | |
| 146 | required: ["repo", "number"], | |
| 147 | }, | |
| 148 | run: async (env, viewer, input) => { | |
| 149 | const path = repoPath(input); | |
| 150 | return path | |
| 151 | ? env.WORK.getIntent(path, Number(input.number), viewer) | |
| 152 | : BAD_REPO; | |
| 153 | }, | |
| 154 | }, | |
| 155 | { | |
| 156 | name: "open_intent", | |
| 157 | description: "State a new goal for a repository.", | |
| 158 | input: { | |
| 159 | type: "object", | |
| 160 | properties: { | |
| 161 | repo: REPO, | |
| 162 | title: { type: "string", description: "The goal in one line." }, | |
| 163 | brief: { | |
| 164 | type: "string", | |
| 165 | description: "What an agent needs to do the work: outcome, constraints, context.", | |
| 166 | }, | |
| 167 | checks: { | |
| 168 | type: "array", | |
| 169 | items: { type: "string" }, | |
| 170 | description: "Commands that must pass for an attempt to be accepted.", | |
| 171 | }, | |
| 172 | }, | |
| 173 | required: ["repo", "title", "brief"], | |
| 174 | }, | |
| 175 | run: authed(async (env, user, input) => { | |
| 176 | const path = repoPath(input); | |
| 177 | if (!path) return BAD_REPO; | |
| 178 | return env.WORK.openIntent(user, path, { | |
| 179 | title: text(input, "title"), | |
| 180 | brief: text(input, "brief"), | |
| 181 | checks: Array.isArray(input.checks) ? input.checks.map(String) : [], | |
| 182 | }); | |
| 183 | }), | |
| 184 | }, | |
| 185 | { | |
| 186 | name: "start_attempt", | |
| 187 | description: | |
| 188 | "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.", | |
| 189 | input: { | |
| 190 | type: "object", | |
| 191 | properties: { | |
| 192 | intent_id: { type: "string" }, | |
| 193 | agent: { | |
| 194 | type: "string", | |
| 195 | description: 'A label for the agent doing the work, e.g. "claude-code".', | |
| 196 | }, | |
| 197 | }, | |
| 198 | required: ["intent_id"], | |
| 199 | }, | |
| 200 | run: authed(async (env, user, input) => { | |
| 201 | const started = await env.WORK.startAttempt(user, text(input, "intent_id"), { | |
| 202 | agent: text(input, "agent") || "agent", | |
| 203 | runtime: "external", | |
| 204 | }); | |
| 205 | if (!started.ok) return started; | |
| 206 | const { fork } = started.value; | |
| 207 | return ok({ | |
| 208 | attempt: started.value, | |
| 209 | git: { | |
| 210 | remote: `https://g1t.sh/${fork.namespace}/${fork.name}.git`, | |
| 211 | username: user.username, | |
| 212 | password: "your g1t access token", | |
| 213 | }, | |
| 214 | }); | |
| 215 | }), | |
| 216 | }, | |
| 217 | { | |
| 218 | name: "get_attempt", | |
| 219 | description: "An attempt's status, head commit and the intent it belongs to.", | |
| 220 | input: { | |
| 221 | type: "object", | |
| 222 | properties: { attempt_id: { type: "string" } }, | |
| 223 | required: ["attempt_id"], | |
| 224 | }, | |
| 225 | run: (env, viewer, input) => | |
| 226 | env.WORK.getAttempt(text(input, "attempt_id"), viewer), | |
| 227 | }, | |
| 228 | { | |
| 229 | name: "record_session", | |
| 230 | description: | |
| 231 | "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.", | |
| 232 | input: { | |
| 233 | type: "object", | |
| 234 | properties: { | |
| 235 | attempt_id: { type: "string" }, | |
| 236 | entries: { | |
| 237 | type: "array", | |
| 238 | items: { | |
| 239 | type: "object", | |
| 240 | properties: { | |
| 241 | kind: { | |
| 242 | type: "string", | |
| 243 | enum: ["prompt", "message", "tool_call", "tool_result", "note"], | |
| 244 | }, | |
| 245 | text: { type: "string" }, | |
| 246 | tool: { type: "string", description: "Tool name, for tool entries." }, | |
| 247 | }, | |
| 248 | required: ["kind", "text"], | |
| 249 | }, | |
| 250 | }, | |
| 251 | }, | |
| 252 | required: ["attempt_id", "entries"], | |
| 253 | }, | |
| 254 | run: authed(async (env, user, input) => { | |
| 255 | if (!Array.isArray(input.entries)) { | |
| 256 | return fail("invalid", "entries must be an array."); | |
| 257 | } | |
| 258 | return env.WORK.appendSession( | |
| 259 | user, | |
| 260 | text(input, "attempt_id"), | |
| 261 | input.entries as NewSessionEntry[], | |
| 262 | ); | |
| 263 | }), | |
| 264 | }, | |
| 265 | { | |
| 266 | name: "read_session", | |
| 267 | description: "The recorded session of an attempt, oldest entry first.", | |
| 268 | input: { | |
| 269 | type: "object", | |
| 270 | properties: { | |
| 271 | attempt_id: { type: "string" }, | |
| 272 | after: { type: "integer", description: "Only entries after this sequence number." }, | |
| 273 | }, | |
| 274 | required: ["attempt_id"], | |
| 275 | }, | |
| 276 | run: (env, viewer, input) => | |
| 277 | env.WORK.readSession( | |
| 278 | text(input, "attempt_id"), | |
| 279 | viewer, | |
| 280 | Number(input.after) || 0, | |
| 281 | ), | |
| 282 | }, | |
| 283 | { | |
| 284 | name: "submit_attempt", | |
| 285 | description: | |
| 286 | "Mark an attempt ready to be compared and shipped. Push your commits first. The summary should say what changed and why.", | |
| 287 | input: { | |
| 288 | type: "object", | |
| 289 | properties: { attempt_id: { type: "string" }, summary: { type: "string" } }, | |
| 290 | required: ["attempt_id", "summary"], | |
| 291 | }, | |
| 292 | run: authed((env, user, input) => | |
| 293 | env.WORK.submitAttempt(user, text(input, "attempt_id"), text(input, "summary")), | |
| 294 | ), | |
| 295 | }, | |
| 296 | { | |
| 297 | name: "abandon_attempt", | |
| 298 | description: "Give up on an attempt.", | |
| 299 | input: { | |
| 300 | type: "object", | |
| 301 | properties: { attempt_id: { type: "string" } }, | |
| 302 | required: ["attempt_id"], | |
| 303 | }, | |
| 304 | run: authed((env, user, input) => | |
| 305 | env.WORK.abandonAttempt(user, text(input, "attempt_id")), | |
| 306 | ), | |
| 307 | }, | |
| 308 | { | |
| docs.g1t.sh, generated OpenAPI with an interactive reference, full footer | 309 | name: "get_attempt_changes", |
| 310 | description: | |
| 311 | "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.", | |
| 312 | input: { | |
| 313 | type: "object", | |
| 314 | properties: { attempt_id: { type: "string" } }, | |
| 315 | required: ["attempt_id"], | |
| 316 | }, | |
| 317 | run: async (env, viewer, input) => { | |
| 318 | const found = await env.WORK.getAttempt(text(input, "attempt_id"), viewer); | |
| 319 | if (!found.ok) return found; | |
| 320 | const { forkRepoId, landedBase } = found.value.attempt; | |
| 321 | return env.REPOS.compare(forkRepoId, viewer, landedBase); | |
| 322 | }, | |
| 323 | }, | |
| 324 | { | |
| Rust repos service with shipping; pull requests kept in the model | 325 | name: "ship_attempt", |
| 326 | description: | |
| 327 | "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.", | |
| 328 | input: { | |
| 329 | type: "object", | |
| 330 | properties: { attempt_id: { type: "string" } }, | |
| 331 | required: ["attempt_id"], | |
| 332 | }, | |
| 333 | run: authed((env, user, input) => | |
| 334 | env.WORK.shipAttempt(user, text(input, "attempt_id")), | |
| 335 | ), | |
| 336 | }, | |
| 337 | { | |
| API and MCP server, Rust identity service, registration, site redesign | 338 | name: "list_events", |
| 339 | description: | |
| 340 | "The timeline of a repository: pushes, intents, attempts and session activity, newest first.", | |
| 341 | input: { | |
| 342 | type: "object", | |
| 343 | properties: { | |
| 344 | repo: REPO, | |
| 345 | before: { type: "string", description: "Event id to page back from." }, | |
| 346 | }, | |
| 347 | required: ["repo"], | |
| 348 | }, | |
| 349 | run: async (env, viewer, input) => { | |
| 350 | const path = repoPath(input); | |
| 351 | if (!path) return BAD_REPO; | |
| 352 | const repo = await env.REPOS.get(path, viewer); | |
| 353 | if (!repo.ok) return repo; | |
| 354 | return ok( | |
| 355 | await env.EVENTS.list({ | |
| 356 | repoId: repo.value.id, | |
| 357 | before: text(input, "before") || undefined, | |
| 358 | }), | |
| 359 | ); | |
| 360 | }, | |
| 361 | }, | |
| 362 | ]; | |
| 363 | ||
| 364 | export const operationsByName = new Map( | |
| 365 | operations.map((operation) => [operation.name, operation]), | |
| 366 | ); |