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,302 linesCodeBlameRaw
1import type { ChecksApi } from "./checks";
2import type { CodeownersReport, PullCodeOwners } from "./codeowners";
3import type { G1tEvent } from "./events";
4import type { User, Viewer } from "./identity";
5import type { PullBranchUpdate, RepoPath } from "./repos";
6import type { Result } from "./result";
7import type { MergeRules, RulesApi } from "./rules";
8
9/**
10 * The filter on lists of issues and pull requests. An open pull request is
11 * a draft or one ready for review; a closed one was merged or closed
12 * without merging.
13 */
14export type State = "open" | "closed";
15
16/** Why an issue was closed. */
17export type IssueReason = "completed" | "not_planned";
18
19/**
20 * Something that should change in a repository: a bug, a feature, a
21 * question. Opened by a person, an agent or an integration. Pull requests
22 * are made against it; the one that is merged resolves it.
23 *
24 * Issues and pull requests share one sequence of numbers per repository.
25 */
26export type Issue = {
27 id: string;
28 repoId: string;
29 /** Shown as `#12`. */
30 number: number;
31 title: string;
32 /** Markdown. Also what an agent is given to work from. */
33 body: string;
34 labels: string[];
35 state: State;
36 /** Set when closed. */
37 reason: IssueReason | null;
38 /** The number of the pull request whose merge closed this issue. */
39 resolvedBy: number | null;
40 /** Who opened it: a person, an integration, or g1t (`kind` `agent`) for one its agent filed while at work. */
41 author: User;
42 /**
43 * For an issue g1t's agent filed: the person it was working for. They may
44 * manage it as its author could. See `workOwner`.
45 */
46 requestedBy: User | null;
47 /** RFC 3339. */
48 createdAt: string;
49 /** RFC 3339. */
50 updatedAt: string;
51 /** RFC 3339. */
52 closedAt: string | null;
53 /** Pull requests made against this issue, in any state. */
54 pullCount: number;
55 commentCount: number;
56 /** Usernames of the people it is assigned to. */
57 assignees: string[];
58 /** The numbers of the issues that have to be merged before this one is worked on. */
59 blockedBy: number[];
60 /**
61 * The agent working on it now: the one behind its newest pull request
62 * that is still in progress in a fork, such as `g1t`.
63 */
64 agent: string | null;
65 /** The milestone it is in, if any. */
66 milestone?: MilestoneRef | null;
67};
68
69/**
70 * A label of a repository: a name, a color and what it means. Issues and
71 * pull requests carry labels by name; names are lowercase.
72 */
73export type Label = {
74 name: string;
75 /** Six hex digits, without `#`. */
76 color: string;
77 description: string;
78 /** How many issues carry it, open or closed. */
79 issues: number;
80 /** How many pull requests carry it, in any state. */
81 pulls: number;
82};
83
84/** A milestone, as an issue or pull request names it. */
85export type MilestoneRef = { number: number; title: string };
86
87/**
88 * A goal, with an optional due date, that issues and pull requests are
89 * gathered under. Its progress is how many of them are closed.
90 */
91export type Milestone = {
92 /** Numbered from 1 in each repository, apart from issues. */
93 number: number;
94 title: string;
95 /** Markdown. */
96 description: string;
97 /** `YYYY-MM-DD`. */
98 dueOn: string | null;
99 state: State;
100 /** Open issues and pull requests in it. */
101 openItems: number;
102 /** Closed issues, and merged or closed pull requests, in it. */
103 closedItems: number;
104 createdAt: string;
105 updatedAt: string;
106 closedAt: string | null;
107};
108
109/** One milestone and what is in it, newest first. */
110export type MilestoneDetail = { milestone: Milestone; issues: Issue[]; pulls: Pull[] };
111
112/** How `setLabels` changes an item's labels. */
113export type LabelChange = "set" | "add" | "remove";
114
115/**
116 * Whose an issue or a pull request is to answer for: whoever asked g1t for
117 * it, or its author. They may change, close and steer it, are never asked to
118 * review it and cannot approve it, and see it as theirs. Mirrors
119 * `Pull::owner` in the Rust contracts.
120 */
121export function workOwner(item: Pick<Pull, "author" | "requestedBy">): User {
122 return item.requestedBy ?? item.author;
123}
124
125/** `draft` is still being worked on; `open` is ready for review. */
126export type PullStatus = "draft" | "open" | "merged" | "closed";
127
128/** Where the agent runs: on g1t's sandboxes, or in someone's own session. */
129export type Runtime = "hosted" | "external";
130
131/**
132 * A proposed change. It is made either in a fork created for it, which is
133 * how agents work, or on a branch pushed to the repository itself.
134 */
135export type Pull = {
136 id: string;
137 repoId: string;
138 /** Shown as `#12`. */
139 number: number;
140 /** The number of the issue this is for, if any. */
141 issue: number | null;
142 title: string;
143 /** Markdown: what changed and why. Set when marked ready. */
144 body: string | null;
145 /** A label for the agent doing the work, e.g. `claude-code`. */
146 agent: string;
147 runtime: Runtime;
148 status: PullStatus;
149 /** The fork holding the change, unless it is on a branch. */
150 fork: RepoPath | null;
151 /** The fork's repository id. */
152 forkRepoId: string | null;
153 /** The branch of the repository holding the change, unless it is in a fork. */
154 branch: string | null;
155 headCommit: string | null;
156 /**
157 * For a merged pull request, what the branch pointed to before the merge.
158 * Comparing against it shows what the pull request changed.
159 */
160 mergeBase: string | null;
161 /** Username of whoever merged it. */
162 mergedBy: string | null;
163 /** RFC 3339. */
164 mergedAt: string | null;
165 /**
166 * Set on a pull request closed because another one for the same issue was
167 * merged: that one's number.
168 */
169 supersededBy: number | null;
170 /**
171 * Where the latest run of the issue's acceptance checks stands, if there
172 * has been one against the current head.
173 */
174 checkStatus: CheckStatus | null;
175 /** The files it changes, as of its latest push. */
176 files: ChangedFile[];
177 /** Usernames of the people it is assigned to. */
178 assignees: string[];
179 /**
180 * Those whose review was asked for: usernames, and `g1t` when a g1t
181 * agent was asked.
182 */
183 reviewers: string[];
184 /**
185 * Teams whose review was asked for, as `workspace/slug`. A team stays here
186 * after review assignment picks people from it, who are in `reviewers`.
187 */
188 teamReviewers?: string[];
189 /** The labels it carries, by name. */
190 labels?: string[];
191 /** The milestone it is in, if any. */
192 milestone?: MilestoneRef | null;
193 /**
194 * The branch it merges into. Lists and `getPull` name it; null only in
195 * what services pass between themselves, for the default branch.
196 */
197 base?: string | null;
198 /** Who opened it: a person, or g1t (`kind` `agent`, username `g1t`) for a change g1t made. */
199 author: User;
200 /**
201 * For a change g1t made: the person who asked for it, by assigning an issue
202 * or handing g1t the work. They answer for it as its author would. See
203 * `workOwner`.
204 */
205 requestedBy: User | null;
206 /** RFC 3339. */
207 createdAt: string;
208 /** RFC 3339. */
209 updatedAt: string;
210 /** Who last moved its head (a user id), and when; absent until a push after rulesets arrived. */
211 headPushedBy?: string;
212 headPushedAt?: string;
213};
214
215/** One file a pull request changes, and by how much. */
216export type ChangedFile = { path: string; additions: number; deletions: number };
217
218/**
219 * Another pull request in progress that changes some of the same files. Two
220 * for the same issue are alternatives; two for different issues are heading
221 * for a conflict.
222 */
223export type Overlap = {
224 number: number;
225 title: string;
226 /** The number of the issue the other pull request is for. */
227 issue: number | null;
228 /** The files both change. */
229 paths: string[];
230};
231
232/**
233 * On a pull request, `failed` means the merge queue took it out, until its
234 * head moves. The other states are from runs of commands written on issues,
235 * which g1t no longer runs.
236 */
237export type CheckStatus = "queued" | "running" | "passed" | "failed" | "errored";
238
239/** How one command went, in a run recorded before checks were workflows. */
240export type CheckResult = {
241 command: string;
242 passed: boolean;
243 /** Null when the command was stopped for taking too long. */
244 exitCode: number | null;
245 /** What the command printed; the end of it, when there was a lot. */
246 output: string;
247 durationMs: number;
248};
249
250/**
251 * A record against a pull request's head: the merge queue taking it out,
252 * with why, or an earlier run of commands written on its issue.
253 */
254export type CheckRun = {
255 id: string;
256 /** The commit that was checked. */
257 headCommit: string;
258 status: CheckStatus;
259 results: CheckResult[];
260 /** Why the checks could not be run, when `status` is `errored`. */
261 error: string | null;
262 /** RFC 3339. */
263 createdAt: string;
264 /** RFC 3339. */
265 finishedAt: string | null;
266};
267
268/** A reviewer's decision on a pull request. */
269export type Verdict = "approve" | "request_changes";
270
271/**
272 * A comment on an issue or a pull request. On a pull request it can sit on
273 * one line of the change, and it can carry a reviewer's verdict.
274 */
275export type Comment = {
276 id: string;
277 /**
278 * Something a person or an agent wrote, or something that happened: an
279 * assignment, a review asked for, a close.
280 */
281 kind: "comment" | "event";
282 author: User;
283 /**
284 * Markdown. For an event, what its author did, as the rest of a sentence
285 * that starts with their name: "assigned ana".
286 */
287 body: string;
288 /** The file commented on, for a comment on a line. */
289 path: string | null;
290 /** The line of that file, as numbered after the change. */
291 line: number | null;
292 verdict: Verdict | null;
293 /** RFC 3339. */
294 createdAt: string;
295 /** When its text was last edited, RFC 3339; absent if it never was. */
296 editedAt?: string;
297 /**
298 * Set when one of the workspace's agents wrote it, as itself. `author`
299 * is then the agent (its id, its handle as `username`, kind `agent`):
300 * show it by `agent.displayName` with an Agent badge, never as a person.
301 */
302 agent?: AgentRef;
303 /** Who the agent acted for, whose access capped it. Set with `agent`. */
304 actingFor?: User;
305 /**
306 * An agent's review. Its `verdict` (none for a review that only
307 * comments) is shown but advisory: it never counts toward required
308 * approvals or code owners, and never blocks a merge.
309 */
310 advisory?: boolean;
311};
312
313/**
314 * One of a workspace's own agents, as a comment or review it wrote before
315 * g1t's agents were removed shows it: Margo (@margo), with the face drawn
316 * from `avatarSeed`. Kept so those comments still render.
317 */
318export type AgentRef = {
319 id: string;
320 handle: string;
321 displayName: string;
322 avatarSeed: string;
323};
324
325export type NewComment = {
326 /** May be empty when approving. */
327 body: string;
328 path?: string;
329 line?: number;
330 verdict?: Verdict;
331};
332
333export type CheckReport = { results?: CheckResult[]; error?: string; skip?: boolean };
334
335export type SessionEntryKind = "prompt" | "message" | "tool_call" | "tool_result" | "note" | "skill";
336
337/** One step of an agent's session: the "why" behind a pull request's commits. */
338export type SessionEntry = {
339 seq: number;
340 kind: SessionEntryKind;
341 text: string;
342 /** For tool calls and results. */
343 tool: string | null;
344 /** The fork's head commit when this entry was recorded, if known. */
345 commit: string | null;
346 /** RFC 3339. */
347 at: string;
348};
349
350export type NewSessionEntry = Pick<SessionEntry, "kind" | "text"> &
351 Partial<Pick<SessionEntry, "tool" | "commit">>;
352
353export type IssueDetail = {
354 issue: Issue;
355 /** Every pull request made against it, oldest first. */
356 pulls: Pull[];
357 comments: Comment[];
358};
359
360export type PullDetail = {
361 pull: Pull;
362 /** The issue it is for, if any. */
363 issue: Issue | null;
364 comments: Comment[];
365 /** The latest record against its head: the merge queue taking it out. */
366 checks: CheckRun | null;
367 /** Other pull requests in progress that change the same files. */
368 overlaps: Overlap[];
369 /**
370 * Whether the branch it would merge into has moved on without it, so that
371 * it has to catch up before it can merge.
372 */
373 behind: boolean;
374 /**
375 * Where it stands on its way to being merged, for a pull request g1t is
376 * seeing through. Null on anyone else's.
377 */
378 lifecycle: Lifecycle | null;
379 /**
380 * A merge was asked for while it was behind: g1t is bringing it up to
381 * date and will then land it.
382 */
383 landing: boolean;
384 /** Why g1t stopped working on it, if it did. */
385 stalled: string | null;
386 /** What workflow runs said about its head commit, one per workflow. */
387 statuses?: CommitStatus[];
388 /**
389 * Whether it merges cleanly into the branch it targets, worked out ahead
390 * of time whenever either side moves.
391 */
392 mergeable?: Mergeable;
393 /** When `mergeable` is `conflicting`: the files that conflict. */
394 conflicts?: string[];
395 /** Earlier records like `checks`, newest first, without their output. */
396 earlierChecks?: CheckRun[];
397 /**
398 * The checks the default branch's protection requires, each as it stands
399 * on the head commit. Empty when none are required.
400 */
401 requiredChecks?: RequiredCheck[];
402 /**
403 * Who owns the files it changes (the CODEOWNERS file of the branch it
404 * merges into) and whose approval is still needed. Absent without one.
405 */
406 codeOwners?: PullCodeOwners | null;
407 /**
408 * The rules of the branch it merges into it does not meet yet, for whoever
409 * is looking: what refuses the merge, what they may bypass, and what rulesets
410 * in evaluate would refuse. Absent once it is closed or merged.
411 */
412 rules?: MergeRules | null;
413};
414
415/** Where a required check stands on a commit; `expected` when nothing has reported it yet. */
416export type RequiredState = "success" | "failure" | "pending" | "expected";
417
418/** One check a branch's protection requires, as it stands on a commit. */
419export type RequiredCheck = {
420 /** A workflow's name, such as `CI`, or another status's context. */
421 name: string;
422 state: RequiredState;
423 description: string | null;
424 /** Where to see more: the workflow run, for one a workflow reported. */
425 targetUrl: string | null;
426};
427
428/** A check name reported on a repository's commits lately, for choosing required checks. */
429export type SeenCheck = {
430 name: string;
431 /** The events it was reported for, such as `pull_request`; empty for a status that names none. */
432 events: string[];
433 /** RFC 3339. */
434 lastSeen: string;
435};
436
437/**
438 * A status context's check name: `CI / pull_request` is the `CI` check,
439 * reported for a `pull_request` event. A context that does not end in an
440 * event, such as `g1t / deploy`, is its own name. As the work service reads it.
441 */
442export function checkName(context: string): string {
443 const at = context.lastIndexOf(" / ");
444 if (at <= 0) return context;
445 return STATUS_EVENTS.has(context.slice(at + 3)) ? context.slice(0, at) : context;
446}
447
448const STATUS_EVENTS = new Set([
449 "push",
450 "pull_request",
451 "pull_request_target",
452 "pull_request_review",
453 "merge_group",
454 "workflow_dispatch",
455 "workflow_run",
456 "workflow_call",
457 "schedule",
458 "release",
459 "issues",
460 "issue_comment",
461 "repository_dispatch",
462]);
463
464/**
465 * Whether a pull request merges cleanly into the branch it targets:
466 * `unknown` when it was never worked out or could not be, `checking` while
467 * a probe merges the two.
468 */
469export type Mergeable = "clean" | "conflicting" | "unknown" | "checking";
470
471/** What a sandbox needs to find out whether a pull request merges cleanly. */
472export type MergecheckJob = {
473 pullId: string;
474 /** Lets the sandbox, and nothing else, report this probe. */
475 token: string;
476 repo: RepoPath;
477 number: number;
478 defaultBranch: string;
479 /** The default branch's commit to merge into. */
480 base: string;
481 /** The repository holding the change: its fork, or the repository. */
482 source: RepoPath;
483 /** The branch of `source` holding it. */
484 branch: string;
485 /** The change's commit. */
486 head: string;
487 /** Who the pull request is for (`workOwner`: whoever asked g1t for it, or its author), and so can read its source. */
488 author: User;
489};
490
491/** What a workflow run (or another tool) says about a commit. */
492export type CommitStatus = {
493 /** What reported it, such as `CI / push`. */
494 context: string;
495 state: "pending" | "success" | "failure" | "error";
496 description: string | null;
497 targetUrl: string | null;
498 updatedAt: string;
499 /** The integration that reported it: `actions`, `deployments`, `security`, `g1t` or `api`. */
500 source?: string;
501 /** Set on the status a check run stands as: the check run's id. */
502 checkRunId?: string;
503};
504
505/**
506 * A step on the way from an assigned issue to a pull request that is ready
507 * to merge. g1t takes each one without being asked: `working` (the agent is
508 * making the change), `checking`, `reviewing`, `revising` (the agent is
509 * addressing failed checks or a review), `catching_up` (merging in the
510 * branch it would land on), `answering` (woken to answer another agent),
511 * then `ready` for a person to merge. `needs_you`
512 * means g1t has stopped and a person decides what happens next.
513 */
514export type Stage =
515 | "working"
516 | "checking"
517 | "reviewing"
518 | "revising"
519 | "catching_up"
520 | "answering"
521 | "queued"
522 | "ready"
523 | "needs_you";
524
525export type Lifecycle = {
526 stage: Stage;
527 /** One sentence saying what is happening, or why it stopped. */
528 detail: string;
529 /** How many times the agent has been sent back to revise it. */
530 revisions: number;
531};
532
533/**
534 * How a repository wants its pull requests handled. A repository that has
535 * changed nothing has the defaults.
536 */
537export type RepoSettings = {
538 /**
539 * Land a g1t agent's pull request without a person once it is ready:
540 * checks passed and approved as the settings below require.
541 */
542 autoMerge: boolean;
543 /**
544 * The checks that must pass on a pull request's head before it may merge
545 * into the default branch, by name: a workflow's name (`CI`) or another
546 * status's context (`g1t / deploy`). The same for people and agents, and
547 * for the merge queue.
548 */
549 requiredChecks: string[];
550 /**
551 * Refuse to merge a pull request that does not contain the default
552 * branch's latest commits, so that what merges is what was checked. When
553 * off, merging one that is behind brings it up to date first.
554 */
555 requireUpToDate: boolean;
556 /**
557 * How many approving reviews a pull request needs before it may merge. A
558 * reviewer who has since asked for changes blocks it.
559 */
560 requiredApprovals: number;
561 /** Whether a g1t agent's approval counts towards `requiredApprovals`. */
562 countAgentApprovals: boolean;
563 /** Whether someone who may merge can bypass required checks that have not passed. */
564 allowIgnoringChecks: boolean;
565 /**
566 * Merge through a queue: pull requests are tested together with those
567 * ahead of them, and only a combination that passed reaches the default
568 * branch.
569 */
570 mergeQueue: boolean;
571 /**
572 * Refuse to merge until the code owners of every file it changes have
573 * approved, as many as each section asks. People only; `g1t` only where
574 * the file names `@g1t`.
575 */
576 requireCodeOwnerReview?: boolean;
577 /** Username of the member who last changed the settings, if anyone has. */
578 updatedBy: string | null;
579 /** RFC 3339. */
580 updatedAt: string | null;
581};
582
583/** The settings a member can change. */
584export type RepoSettingsInput = Omit<RepoSettings, "updatedBy" | "updatedAt">;
585
586/**
587 * What the runner is to do next for a pull request: always nothing now,
588 * since no step of a pull request is carried out in a sandbox any more.
589 */
590export type Advance = { action: "none" };
591
592export type OpenIssueInput = {
593 title: string;
594 body: string;
595 labels?: string[];
596 /** Deprecated: commands, added to the body under "Definition of done". */
597 checks?: string[];
598 /** The number of the milestone to put it in. Needs the Triage role. */
599 milestone?: number;
600};
601
602export type UpdateIssueInput = {
603 title?: string;
604 body?: string;
605 labels?: string[];
606 /** Usernames of the people it is assigned to; replaces the whole set. */
607 assignees?: string[];
608 /** The number of the milestone to put it in; 0 takes it out. */
609 milestone?: number;
610 /** The issues of the same repository that have to land first; replaces the whole set. */
611 blocked_by?: number[];
612};
613
614export type OpenPullInput = {
615 /** The number of the issue this is for. */
616 issue?: number;
617 /** Defaults to the issue's title; required without an issue. */
618 title?: string;
619 /** What changed and why. Usually set later, when a draft is marked ready. */
620 body?: string;
621 /**
622 * A branch of the repository that already holds the change. The pull
623 * request is then ready for review at once and has no fork.
624 */
625 branch?: string;
626 agent: string;
627 runtime: Runtime;
628 /** The branch to merge into: the default branch when left out. */
629 base?: string;
630 /**
631 * Opened from a branch as a draft, still being worked on: it can't merge
632 * until it is marked ready.
633 */
634 draft?: boolean;
635};
636
637/** One repository's pull requests from `pullsForRepos`, newest first. */
638export type RepoPulls = {
639 repoId: string;
640 /** Draft and open. */
641 open: Pull[];
642 /** Merged and closed. */
643 closed: Pull[];
644};
645
646/** Issues, pull requests, comments and sessions. */
647export interface WorkApi extends RulesApi, ChecksApi, CoordinationApi {
648 openIssue(actor: User, repo: RepoPath, input: OpenIssueInput): Promise<Result<Issue>>;
649 /** Newest first. */
650 listIssues(
651 repo: RepoPath,
652 viewer: Viewer,
653 filter?: { state?: State; label?: string; milestone?: number },
654 ): Promise<Result<Issue[]>>;
655 getIssue(repo: RepoPath, number: number, viewer: Viewer): Promise<Result<IssueDetail>>;
656 /** The author or a member of the workspace may. */
657 updateIssue(actor: User, repo: RepoPath, number: number, input: UpdateIssueInput): Promise<Result<Issue>>;
658 closeIssue(actor: User, repo: RepoPath, number: number, reason?: IssueReason): Promise<Result<Issue>>;
659 reopenIssue(actor: User, repo: RepoPath, number: number): Promise<Result<Issue>>;
660 /** A repository's labels, by name, each with how many issues and pull requests carry it. */
661 listLabels(repo: RepoPath, viewer: Viewer): Promise<Result<Label[]>>;
662 /**
663 * Creates a label, or with `name` changes one; renaming it renames it on
664 * everything that carries it. Needs the Triage role.
665 */
666 saveLabel(
667 actor: User,
668 repo: RepoPath,
669 label: { name?: string; newName?: string; color?: string; description?: string },
670 ): Promise<Result<Label>>;
671 /** Removes a label from the repository and everything carrying it. */
672 deleteLabel(actor: User, repo: RepoPath, name: string): Promise<Result<boolean>>;
673 /** Adds the default labels the repository does not have yet; returns them all. */
674 addDefaultLabels(actor: User, repo: RepoPath): Promise<Result<Label[]>>;
675 /**
676 * The labels of an issue or a pull request, replaced, added to or taken
677 * from. Labels the repository lacks are created for someone with the
678 * Triage role. Returns its labels now.
679 */
680 setLabels(
681 actor: User,
682 repo: RepoPath,
683 number: number,
684 labels: string[],
685 change?: LabelChange,
686 ): Promise<Result<string[]>>;
687 /** Open ones by due date, then closed ones; both unless `state` says. */
688 listMilestones(repo: RepoPath, viewer: Viewer, state?: State): Promise<Result<Milestone[]>>;
689 getMilestone(repo: RepoPath, number: number, viewer: Viewer): Promise<Result<MilestoneDetail>>;
690 /** Creates a milestone, or with `number` changes the fields given. Needs the Triage role. */
691 saveMilestone(
692 actor: User,
693 repo: RepoPath,
694 milestone: { number?: number; title?: string; description?: string; dueOn?: string; state?: State },
695 ): Promise<Result<Milestone>>;
696 deleteMilestone(actor: User, repo: RepoPath, number: number): Promise<Result<boolean>>;
697 /** How many issues and pull requests are open. */
698 counts(repo: RepoPath, viewer: Viewer): Promise<Result<{ issues: number; pulls: number }>>;
699
700 /**
701 * On an issue or a pull request. On a pull request it may name a line of
702 * the change and carry a verdict; nobody can give a verdict on their own.
703 */
704 addComment(actor: User, repo: RepoPath, number: number, comment: NewComment): Promise<Result<Comment>>;
705 /**
706 * Changes a comment's text. Its author may, and so may anyone with the
707 * Maintain role or higher; notes of what happened cannot be changed.
708 */
709 editComment(actor: User, repo: RepoPath, commentId: string, body: string): Promise<Result<Comment>>;
710 /**
711 * Deletes a comment: its author, or anyone with the Maintain role or
712 * higher. A review that gave a verdict cannot be deleted, only edited.
713 */
714 deleteComment(actor: User, repo: RepoPath, commentId: string): Promise<Result<boolean>>;
715
716 /**
717 * What a sandbox says about a run from before checks were workflows: so
718 * one still finishing is recorded.
719 */
720 reportChecks(runId: string, token: string, report: CheckReport): Promise<Result<CheckRun>>;
721 /**
722 * Claims the probe of whether a pull request merges cleanly that a
723 * `pull.mergecheck` event asked for. Refused when it is no longer wanted
724 * or the repository has as many running as it may. For the runner service.
725 */
726 startMergecheck(pullId: string): Promise<Result<MergecheckJob>>;
727 /** Records that a probe could not be carried out. For the runner service. */
728 failMergecheck(pullId: string, token: string, error: string): Promise<Result<Mergeable>>;
729
730 /**
731 * Works out the next step for a pull request g1t is seeing through and,
732 * if there is one to take now, claims it, so that it is taken once
733 * however often this is called. For the runner service.
734 */
735 advance(pullId: string): Promise<Advance>;
736 /** Ids of the open pull requests g1t is seeing through. */
737 managedPulls(repoId?: string): Promise<string[]>;
738
739 /** A repository's merge queue: what is in it, in order, and what recently left. */
740 queue(repo: RepoPath, viewer: Viewer): Promise<Result<QueueView>>;
741 /**
742 * The next batch of combined states to test for a repository, one per
743 * entry; empty while a batch is being tested or nothing waits.
744 */
745 queueBuild(repoId: string): Promise<QueueJob[]>;
746 /** Reports that a combined state could not be built or checked. */
747 failQueue(entryId: string, token: string, error: string): Promise<Result<QueueState>>;
748 /** Takes a pull request out of the merge queue. Members only. */
749 removeFromQueue(actor: User, repo: RepoPath, number: number): Promise<Result<Pull>>;
750
751 getSettings(repo: RepoPath, viewer: Viewer): Promise<Result<RepoSettings>>;
752 /** The check names reported on the repository's commits in the last 30 days, most recent first. */
753 seenChecks(repo: RepoPath, viewer: Viewer): Promise<Result<SeenCheck[]>>;
754 /** Members of the repository's workspace only. */
755 updateSettings(actor: User, repo: RepoPath, settings: RepoSettingsInput): Promise<Result<RepoSettings>>;
756 /** The CODEOWNERS file at a branch (the default when left out), checked. Needs Read. */
757 codeownersErrors(repo: RepoPath, viewer: Viewer, ref?: string | null): Promise<Result<CodeownersReport>>;
758
759 /**
760 * Opens a pull request: a draft with a fork to push to, or, given a
761 * branch, one ready for review.
762 */
763 openPull(actor: User, repo: RepoPath, input: OpenPullInput): Promise<Result<Pull>>;
764 /** Newest first. */
765 listPulls(
766 repo: RepoPath,
767 viewer: Viewer,
768 state?: State,
769 filter?: { label?: string; milestone?: number; base?: string },
770 ): Promise<Result<Pull[]>>;
771 /**
772 * The newest `limit` open and closed pull requests of each repository,
773 * in one call. Repositories the viewer cannot read, and forks, are left
774 * out: ask those with `listPulls`.
775 */
776 pullsForRepos(repoIds: string[], viewer: Viewer, limit: number): Promise<RepoPulls[]>;
777 getPull(repo: RepoPath, number: number, viewer: Viewer): Promise<Result<PullDetail>>;
778 /**
779 * Changes who a pull request is assigned to and whose review is asked
780 * for; each list given replaces the whole set. Asking for `g1t`'s
781 * review does not by itself start one: the runner's `review` does.
782 */
783 updatePull(
784 actor: User,
785 repo: RepoPath,
786 number: number,
787 changes: {
788 assignees?: string[];
789 reviewers?: string[];
790 labels?: string[];
791 /** 0 takes it out of its milestone. */
792 milestone?: number;
793 /** The branch it merges into. Needs the Write role. */
794 base?: string;
795 },
796 ): Promise<Result<Pull>>;
797 /**
798 * Brings a pull request up to date with the default branch in seconds,
799 * without a sandbox, when the two changed different files: the merge
800 * commit is pushed to its branch as `actor`, who must be whoever opened
801 * it (for a fork) or a member (for a branch). Otherwise `needs_agent`, and
802 * nothing is pushed: the runner's `update` is the way on.
803 */
804 catchUpPull(actor: User, repo: RepoPath, number: number): Promise<Result<PullBranchUpdate>>;
805 /** Marks a draft ready for review and sets its description. */
806 readyPull(actor: User, repo: RepoPath, number: number, summary: string): Promise<Result<Pull>>;
807 closePull(actor: User, repo: RepoPath, number: number): Promise<Result<Pull>>;
808 /** Opens a closed pull request again, as the draft it was if it was closed as one. Never a merged one. */
809 reopenPull(actor: User, repo: RepoPath, number: number): Promise<Result<Pull>>;
810 /** Turns an open pull request back into a draft; it leaves the merge queue. */
811 convertPullToDraft(actor: User, repo: RepoPath, number: number): Promise<Result<Pull>>;
812 /**
813 * Lands the pull request on the repository's default branch. Unless
814 * `keepIssueOpen`, that resolves the issue it was for: the issue closes
815 * naming this pull request, and the others still in progress for it close
816 * as superseded. Only members of the repository's workspace may merge.
817 *
818 * If the default branch has moved, the pull request is brought up to date
819 * first and lands when that is done; it comes back still open, and
820 * `PullDetail.landing` is true meanwhile. A repository that requires pull
821 * requests to be up to date refuses instead.
822 */
823 mergePull(
824 actor: User,
825 repo: RepoPath,
826 number: number,
827 options?: {
828 keepIssueOpen?: boolean;
829 /** Merge although required checks have not passed, where the rule requiring them allows it. */
830 ignoreChecks?: boolean;
831 /** Merge past rules a ruleset lets the actor bypass. Recorded as a bypass. */
832 bypassRules?: boolean;
833 },
834 ): Promise<Result<Pull>>;
835 /**
836 * Drafts and open pull requests the viewer started, most recently active
837 * first, each with where it stands if g1t is seeing it through.
838 */
839 listActivePulls(
840 viewer: Viewer,
841 ): Promise<{ pull: Pull; issue: Issue | null; lifecycle: Lifecycle | null }[]>;
842 /**
843 * The issues and pull requests a person opened, a page at a time, only
844 * on repositories the viewer may read. Not found for no such account.
845 */
846 byAuthor(username: string, viewer: Viewer, filter?: AuthoredFilter): Promise<Result<Authored>>;
847 /**
848 * What a person did each day of the last year (issues and pull requests
849 * opened, reviews given), only on repositories the viewer may read: the
850 * calendar on their profile. Not found for no such account.
851 */
852 contributions(username: string, viewer: Viewer): Promise<Result<Contributions>>;
853
854
855
856 appendSession(actor: User, repo: RepoPath, number: number, entries: NewSessionEntry[]): Promise<Result<{ count: number }>>;
857 readSession(repo: RepoPath, number: number, viewer: Viewer, afterSeq?: number): Promise<Result<SessionEntry[]>>;
858}
859
860/**
861 * What to pass `ReposApi.compare` to see what a pull request changes.
862 *
863 * A fork is compared as a whole. A branch is compared by name while the
864 * pull request is open, and by the commit it was merged or closed at
865 * afterwards, so later pushes to the branch do not change the record.
866 */
867export function pullComparison(pull: Pull): {
868 repoId: string;
869 base: string | null;
870 head: string | null;
871 /** The branch it merges into, which it is compared from. */
872 baseBranch: string | null;
873} {
874 const baseBranch = pull.base ?? null;
875 if (pull.forkRepoId) return { repoId: pull.forkRepoId, base: pull.mergeBase, head: null, baseBranch };
876 const settled = pull.status === "merged" || pull.status === "closed";
877 return {
878 repoId: pull.repoId,
879 base: pull.mergeBase,
880 head: (settled && pull.headCommit) || pull.branch,
881 baseBranch,
882 };
883}
884
885
886export type QueueState = "waiting" | "testing" | "passed" | "failed" | "landed" | "removed";
887
888/** One pull request's place in a merge queue. */
889export type QueueEntry = {
890 id: string;
891 number: number;
892 title: string;
893 agent: string;
894 state: QueueState;
895 /** The pull requests merged ahead of it in the state being tested, in order. */
896 ahead: number[];
897 baseCommit: string | null;
898 combinedCommit: string | null;
899 error: string | null;
900 results: CheckResult[];
901 /** Username of whoever merged it into the queue: a person, or `g1t`. */
902 enqueuedBy: string;
903 /** RFC 3339. */
904 createdAt: string;
905 /** RFC 3339. */
906 finishedAt: string | null;
907};
908
909/** A repository's merge queue: what is in it, in order, and what recently left. */
910export type QueueView = {
911 enabled: boolean;
912 active: QueueEntry[];
913 /** Newest first. */
914 recent: QueueEntry[];
915};
916
917export type QueueStackItem = {
918 number: number;
919 title: string;
920 /** The repository holding the change, and its branch. */
921 source: RepoPath;
922 branch: string;
923 commit: string;
924};
925
926/** What a sandbox needs to build and check one combined state. */
927export type QueueJob = {
928 entryId: string;
929 token: string;
930 repo: RepoPath;
931 defaultBranch: string;
932 baseCommit: string;
933 branch: string;
934 stack: QueueStackItem[];
935 /** Always empty: the state is checked by the merge_group workflows run on it. */
936 checks: string[];
937 /** Always empty, as `checks`. */
938 contractChecks: string[];
939 actor: User;
940};
941
942// --- A person's work ---------------------------------------------------------
943// Mirrors the `Authored*` types in `crates/contracts/src/work.rs`.
944
945export type AuthoredKind = "issue" | "pull";
946/** `closed` takes in merged pull requests too; `merged` is only those. */
947export type AuthoredState = "open" | "closed" | "merged";
948/** `created` is newest first, `updated` most recently changed, `oldest` oldest first. */
949export type AuthoredSort = "created" | "updated" | "oldest";
950
951/** The most items one `byAuthor` page holds. */
952export const AUTHORED_PAGE = 25;
953
954export type AuthoredFilter = {
955 kind?: AuthoredKind;
956 state?: AuthoredState;
957 /** `namespace/name`. */
958 repo?: string;
959 sort?: AuthoredSort;
960 /** The `next` of the page before. */
961 before?: string;
962 limit?: number;
963};
964
965export type AuthoredItem = {
966 kind: AuthoredKind;
967 repo: RepoPath;
968 number: number;
969 title: string;
970 /** A merged pull request is closed. */
971 state: State;
972 /** A pull request's own status; null on an issue. */
973 status: PullStatus | null;
974 /** Why an issue was closed. */
975 reason: IssueReason | null;
976 draft: boolean;
977 merged: boolean;
978 /** RFC 3339. */
979 createdAt: string;
980 /** RFC 3339. */
981 updatedAt: string;
982 /** RFC 3339. */
983 mergedAt: string | null;
984};
985
986/** Over every repository the viewer may read, whatever the filters. */
987export type AuthoredCounts = {
988 pullsMerged: number;
989 pullsOpen: number;
990 pulls: number;
991 issues: number;
992 issuesOpen: number;
993};
994
995export type Authored = {
996 items: AuthoredItem[];
997 /** Pass as `before` for the next page; null on the last. */
998 next: string | null;
999 counts: AuthoredCounts;
1000 /** The repositories they worked in that the viewer may read, most work first. */
1001 repos: { repo: RepoPath; count: number }[];
1002};
1003
1004// Mirrors `Contributions` in `crates/contracts/src/work.rs`.
1005
1006/** How many days `contributions` covers: today and the 364 before it. */
1007export const CONTRIBUTION_DAYS = 365;
1008
1009/** One day with something on it; days with nothing are left out. */
1010export type ContributionDay = {
1011 /** `YYYY-MM-DD`, UTC. */
1012 date: string;
1013 /** Everything that day, commits included. */
1014 count: number;
1015 /** How many of `count` are commits they pushed to a default branch. */
1016 commits?: number;
1017};
1018
1019/** A person's year, as far as the viewer may see. */
1020export type Contributions = {
1021 /** Oldest first. */
1022 days: ContributionDay[];
1023 total: number;
1024 /** The first day counted, `YYYY-MM-DD`; the last is today (UTC). */
1025 from: string;
1026};
1027
1028// --- Coordinating work: sessions, claims and dependencies -----------------
1029// Mirrors `crates/contracts/src/work/coordination.rs`.
1030
1031/** The harness a session runs in. */
1032export type Harness = "claude_code" | "claude_managed_agents" | "codex" | "cursor" | "copilot" | "other";
1033
1034/** Active: heard from in 15 minutes; idle: not since; ended: ended, or quiet for 2 hours. */
1035export type SessionState = "active" | "idle" | "ended";
1036
1037/** One run of someone's own harness on a repository. */
1038export type WorkSession = {
1039 /** `ses_…`. */
1040 id: string;
1041 repoId: string;
1042 issue: number | null;
1043 pull: number | null;
1044 branch: string | null;
1045 actor: { id: string; username: string };
1046 harness: Harness;
1047 harnessSessionId: string | null;
1048 title: string | null;
1049 state: SessionState;
1050 headCommit: string | null;
1051 startedAt: string;
1052 lastSeenAt: string;
1053 endedAt: string | null;
1054 /** What the harness said, or `timed_out`. */
1055 endReason: string | null;
1056 /** For a subagent: the session that started it. */
1057 parentId?: string | null;
1058 /** For a subagent: its kind (Explore, security-reviewer). */
1059 agentType?: string | null;
1060 /** For a subagent: the task it was given, in a few words. */
1061 task?: string | null;
1062 /** For a subagent: the prompt its parent gave it. */
1063 prompt?: string | null;
1064 /** What it reported when it finished: a subagent's final message. */
1065 result?: string | null;
1066 model?: string | null;
1067 /** How many subagents it started. */
1068 subagents?: number;
1069 /** In a list, for one with no title: the start of the first prompt it recorded. */
1070 firstPrompt?: string | null;
1071};
1072
1073/** What kind of thing a session touched. */
1074export type TouchKind = "file" | "commit" | "branch" | "pull" | "issue";
1075
1076/** Something a session touched, kept once per session. */
1077export type SessionTouch = { sessionId: string; kind: TouchKind; value: string; at: string };
1078
1079/** How a session was tied to an issue or pull request. */
1080export type LinkVia = "attached" | "claim" | "branch" | "commit" | "trailer" | "opened" | "ref";
1081
1082/** An issue or pull request a session, or one of its subagents, worked on. */
1083export type SessionLink = {
1084 kind: "issue" | "pull";
1085 number: number;
1086 title: string;
1087 /** The issue's state or the pull request's status. */
1088 state: string;
1089 via: LinkVia[];
1090 sessionIds: string[];
1091};
1092
1093/** A skill a session used, by name. */
1094export type SessionSkill = { sessionId: string; name: string; uses: number };
1095
1096export type ClaimState = "active" | "released" | "expired" | "handed_off";
1097
1098/** An advisory, expiring "I'm working on #N". Never a lock. */
1099export type Claim = {
1100 /** `clm_…`. */
1101 id: string;
1102 repoId: string;
1103 issue: number;
1104 sessionId: string | null;
1105 actor: { id: string; username: string };
1106 harness: Harness | null;
1107 note: string | null;
1108 state: ClaimState;
1109 createdAt: string;
1110 expiresAt: string;
1111 endedAt: string | null;
1112 endReason: string | null;
1113 /** For a hand-off: who it was handed to; null for anyone. */
1114 handoffTo: string | null;
1115};
1116
1117/** `session.started` and `session.ended`. */
1118export type SessionEventData = { repoId: string; number?: number; issue?: number; session: WorkSession };
1119
1120/** `work.claimed`, `work.released`, `work.claim_expired` and `work.handoff_requested`. */
1121export type ClaimEventData = {
1122 repoId: string;
1123 number: number;
1124 issue: number;
1125 claim: Claim;
1126 others?: Claim[];
1127 tookOver?: string;
1128 reason?: string;
1129 from?: string;
1130 to?: string;
1131 note?: string;
1132};
1133
1134/** `dependency.changed`. */
1135export type DependencyChangedData = {
1136 repoId: string;
1137 number: number;
1138 issue: number;
1139 blockedBy: number[];
1140 added: number[];
1141 removed: number[];
1142 cleared: number[];
1143 remaining: number[];
1144 mergedPull?: number;
1145};
1146
1147/** `overlap.detected` between two pull requests, sent once for each side of a new overlap. */
1148export type PullOverlapData = {
1149 repoId: string;
1150 number: number;
1151 pull: number;
1152 otherPull: number;
1153 files: string[];
1154 issue?: number;
1155 otherIssue?: number;
1156};
1157
1158/** `overlap.detected` between two sessions that touched the same files (siblings included), once for each side. */
1159export type SessionOverlapData = {
1160 repoId: string;
1161 number?: number;
1162 sessionId: string;
1163 otherSessionId: string;
1164 otherActor: string;
1165 files: string[];
1166 issue?: number;
1167 otherIssue?: number;
1168};
1169
1170/** `overlap.detected`: between pull requests, or between sessions (with `sessionId`). */
1171export type OverlapDetectedData = PullOverlapData | SessionOverlapData;
1172
1173/** `session.progressed`: a session recorded more (at most every few seconds). */
1174export type SessionProgressedData = { repoId: string; number?: number; sessionId: string; rootId?: string };
1175
1176/** An issue in brief. */
1177export type IssueBrief = { number: number; title: string; state: State };
1178
1179/** A pull request in brief. */
1180export type PullBrief = { number: number; title: string; status: PullStatus };
1181
1182/** An issue another is blocked by, and where it stands. */
1183export type Blocker = {
1184 number: number;
1185 title: string;
1186 state: State;
1187 /** Active claims on it. */
1188 claims: Claim[];
1189 pulls: PullBrief[];
1190};
1191
1192/** A pull request's place in the merge queue; `position` 1 is the front. */
1193export type QueuePlace = { state: QueueState; position: number };
1194
1195/** One of an issue's pull requests, with where it stands. */
1196export type WorkPull = {
1197 number: number;
1198 title: string;
1199 status: PullStatus;
1200 branch: string | null;
1201 headCommit: string | null;
1202 author: { id: string; username: string };
1203 /** `failed` when the merge queue took it out, until its head moves. */
1204 checkStatus: CheckStatus | null;
1205 requiredChecks: RequiredCheck[];
1206 mergeable: Mergeable;
1207 /** Other pull requests in progress changing the same files. */
1208 overlaps: Overlap[];
1209 /** Its place in the merge queue, while it is in it. */
1210 queue: QueuePlace | null;
1211};
1212
1213// --- Decision records (crates/contracts/src/decisions.rs) ------------------
1214
1215export type DecisionKind =
1216 | "proposal"
1217 | "claim"
1218 | "integration_failure"
1219 | "integration_passed"
1220 | "fix"
1221 | "approval"
1222 | "merge"
1223 | "note";
1224
1225/** Claimed: someone said so. Tested: a check or the merge queue proved it. Approved: a person approved it. */
1226export type DecisionConfidence = "claimed" | "tested" | "approved";
1227
1228/** A structured reason kept on an issue or pull request, with how sure it is. */
1229export type Decision = {
1230 /** `dec_…`. */
1231 id: string;
1232 issue: number | null;
1233 pull: number | null;
1234 sessionId: string | null;
1235 /** Username of who recorded it; null when g1t did. */
1236 actor: string | null;
1237 kind: DecisionKind;
1238 confidence: DecisionConfidence;
1239 summary: string;
1240 /**
1241 * What it rests on, kept as recorded, in snake_case: `commit`, `check`
1242 * (`name`, `conclusion`, `url`), `combination`, `other_pulls`,
1243 * `suspects`, `bisected`, `merge_queue`.
1244 */
1245 evidence: Record<string, unknown>;
1246 /** RFC 3339. */
1247 createdAt: string;
1248};
1249
1250/** Everything about an issue an agent needs to pick it up: `get_work`. */
1251export type WorkContext = {
1252 issue: Issue;
1253 /** The items under its "Definition of done" heading. */
1254 definitionOfDone: string[];
1255 /** The checks every pull request into the default branch must pass. */
1256 requiredChecks: string[];
1257 /** Active claims on it. */
1258 claims: Claim[];
1259 /** Sessions on it or its pull requests, most recently heard from first. */
1260 sessions: WorkSession[];
1261 pulls: WorkPull[];
1262 blockedBy: Blocker[];
1263 /** The issues it blocks. */
1264 blocks: IssueBrief[];
1265 /** On it and its pull requests, newest first. */
1266 decisions: Decision[];
1267 /** The latest `integration_failure` decision among them. */
1268 lastIntegrationFailure: Decision | null;
1269 /** Its latest coordination events, newest first. */
1270 recent: G1tEvent[];
1271};
1272
1273/** A harness session and what it recorded. */
1274export type WorkSessionDetail = {
1275 session: WorkSession;
1276 entries: SessionEntry[];
1277 /** Its subagents and theirs, in the order they started. */
1278 children?: WorkSession[];
1279 /** What it and its subagents touched. */
1280 touched?: SessionTouch[];
1281 /** The issues and pull requests it and its subagents worked on. */
1282 links?: SessionLink[];
1283 skills?: SessionSkill[];
1284 /** For a subagent: the session at the top of its tree. */
1285 root?: WorkSession | null;
1286};
1287
1288/** Reading coordination: work service methods (services/work/src/coordination, decisions.rs). */
1289export interface CoordinationApi {
1290 getWork(repo: RepoPath, number: number, viewer: Viewer): Promise<Result<WorkContext>>;
1291 /** Newest first; active ones unless `state` says otherwise. */
1292 listClaims(repo: RepoPath, viewer: Viewer, filter?: { state?: ClaimState; issue?: number }): Promise<Result<Claim[]>>;
1293 /** Most recently heard from first. */
1294 listWorkSessions(
1295 repo: RepoPath,
1296 viewer: Viewer,
1297 filter?: { state?: SessionState; issue?: number; pull?: number; path?: string; subagents?: boolean },
1298 ): Promise<Result<WorkSession[]>>;
1299 getWorkSession(repo: RepoPath, id: string, viewer: Viewer, afterSeq?: number): Promise<Result<WorkSessionDetail>>;
1300 /** An issue's (with its pull requests') or a pull request's, oldest first. */
1301 listDecisions(repo: RepoPath, number: number, viewer: Viewer): Promise<Result<Decision[]>>;
1302}